1<?php
2
3namespace dokuwiki\Menu\Item;
4
5/**
6 * Class AbstractItem
7 *
8 * This class defines a single Item to be displayed in one of DokuWiki's menus. Plugins
9 * can extend those menus through action plugins and add their own instances of this class,
10 * overwriting some of its properties.
11 *
12 * Items may be shown multiple times in different contexts. Eg. for the default template
13 * all menus are shown in a Dropdown list on mobile, but are split into several places on
14 * desktop. The item's $context property can be used to hide the item depending on the current
15 * context.
16 *
17 * Children usually just need to overwrite the different properties, but for complex things
18 * the accessors may be overwritten instead.
19 */
20abstract class AbstractItem
21{
22    /** menu item is to be shown on desktop screens only */
23    public const CTX_DESKTOP = 1;
24    /** menu item is to be shown on mobile screens only */
25    public const CTX_MOBILE = 2;
26    /** menu item is to be shown in all contexts */
27    public const CTX_ALL = 3;
28
29    /** @var string name of the action, usually the lowercase class name */
30    protected $type = '';
31    /** @var string optional keyboard shortcut */
32    protected $accesskey = '';
33    /** @var string the page id this action links to */
34    protected $id = '';
35    /** @var string the method to be used when this action is used in a form */
36    protected $method = 'get';
37    /** @var array parameters for the action (should contain the do parameter) */
38    protected $params = [];
39    /** @var bool when true, a rel=nofollow should be used */
40    protected $nofollow = true;
41    /** @var string this item's label may contain a placeholder, which is replaced with this */
42    protected $replacement = '';
43    /** @var string the full path to the SVG icon of this menu item */
44    protected $svg = DOKU_INC . 'lib/images/menu/00-default_checkbox-blank-circle-outline.svg';
45    /** @var string can be set to overwrite the default lookup in $lang.btn_* */
46    protected $label = '';
47    /** @var string the tooltip title, defaults to $label */
48    protected $title = '';
49    /** @var int the context this titme is shown in */
50    protected $context = self::CTX_ALL;
51
52    /**
53     * AbstractItem constructor.
54     *
55     * Sets the dynamic properties
56     *
57     * Children should always call the parent constructor!
58     *
59     * @throws \RuntimeException when the action is disabled
60     */
61    public function __construct()
62    {
63        global $ID;
64        $this->id = $ID;
65        $this->type = $this->getType();
66        $this->params['do'] = $this->type;
67
68        if (!actionOK($this->type)) throw new \RuntimeException("action disabled: {$this->type}");
69    }
70
71    /**
72     * Return this item's label
73     *
74     * When the label property was set, it is simply returned. Otherwise, the action's type
75     * is used to look up the translation in the main language file and, if used, the replacement
76     * is applied.
77     *
78     * @return string
79     */
80    public function getLabel()
81    {
82        if ($this->label !== '') return $this->label;
83
84        /** @var array $lang */
85        global $lang;
86        $label = $lang['btn_' . $this->type];
87        if (strpos($label, '%s')) {
88            $label = sprintf($label, $this->replacement);
89        }
90        if ($label === '') $label = '[' . $this->type . ']';
91        return $label;
92    }
93
94    /**
95     * Return this item's title
96     *
97     * This title should be used to display a tooltip (using the HTML title attribute). If
98     * a title property was not explicitly set, the label will be returned.
99     *
100     * @return string
101     */
102    public function getTitle()
103    {
104        if ($this->title === '') return $this->getLabel();
105        return $this->title;
106    }
107
108    /**
109     * Return the link this item links to
110     *
111     * Basically runs wl() on $id and $params. However if the ID is a hash it is used directly
112     * as the link
113     *
114     * Please note that the generated URL is *not* XML escaped.
115     *
116     * @return string
117     * @see wl()
118     */
119    public function getLink()
120    {
121        if ($this->id && $this->id[0] == '#') {
122            return $this->id;
123        } else {
124            return wl($this->id, $this->params, false, '&');
125        }
126    }
127
128    /**
129     * Convenience method to get the attributes for constructing an <a> element
130     *
131     * @param string|false $classprefix create a class from type with this prefix, false for no class
132     * @return array
133     * @see buildAttributes()
134     */
135    public function getLinkAttributes($classprefix = 'menuitem ')
136    {
137        $attr = ['href' => $this->getLink(), 'title' => $this->getTitle()];
138        if ($this->isNofollow()) $attr['rel'] = 'nofollow';
139        if ($this->getAccesskey()) {
140            $attr['accesskey'] = $this->getAccesskey();
141            $attr['title'] .= ' [' . $this->getAccesskey() . ']';
142        }
143        if ($classprefix !== false) $attr['class'] = $classprefix . $this->getType();
144
145        return $attr;
146    }
147
148    /**
149     * Convenience method to create a full <a> element
150     *
151     * Wraps around the label and SVG image
152     *
153     * @param string|false $classprefix create a class from type with this prefix, false for no class
154     * @param bool $svg add SVG icon to the link
155     * @return string
156     */
157    public function asHtmlLink($classprefix = 'menuitem ', $svg = true)
158    {
159        $attr = buildAttributes($this->getLinkAttributes($classprefix));
160        $html = "<a $attr>";
161        if ($svg) {
162            $html .= '<span>' . hsc($this->getLabel()) . '</span>';
163            $html .= inlineSVG($this->getSvg());
164        } else {
165            $html .= hsc($this->getLabel());
166        }
167        $html .= "</a>";
168
169        return $html;
170    }
171
172    /**
173     * Convenience method to create a <button> element inside it's own form element
174     *
175     * Uses html_btn()
176     *
177     * @return string
178     */
179    public function asHtmlButton()
180    {
181        return html_btn(
182            $this->getType(),
183            $this->id,
184            $this->getAccesskey(),
185            $this->getParams(),
186            $this->method,
187            $this->getTitle(),
188            $this->getLabel(),
189            $this->getSvg()
190        );
191    }
192
193    /**
194     * Should this item be shown in the given context
195     *
196     * @param int $ctx the current context
197     * @return bool
198     */
199    public function visibleInContext($ctx)
200    {
201        return (bool)($ctx & $this->context);
202    }
203
204    /**
205     * @return string the name of this item
206     */
207    public function getType()
208    {
209        if ($this->type === '') {
210            $this->type = strtolower(substr(strrchr(get_class($this), '\\'), 1));
211        }
212        return $this->type;
213    }
214
215    /**
216     * @return string
217     */
218    public function getAccesskey()
219    {
220        return $this->accesskey;
221    }
222
223    /**
224     * @return array
225     */
226    public function getParams()
227    {
228        return $this->params;
229    }
230
231    /**
232     * @return bool
233     */
234    public function isNofollow()
235    {
236        return $this->nofollow;
237    }
238
239    /**
240     * @return string
241     */
242    public function getSvg()
243    {
244        return $this->svg;
245    }
246
247    /**
248     * Return this Item's settings as an array as used in tpl_get_action()
249     *
250     * @return array
251     */
252    public function getLegacyData()
253    {
254        return [
255            'accesskey' => $this->accesskey ?: null,
256            'type' => $this->type,
257            'id' => $this->id,
258            'method' => $this->method,
259            'params' => $this->params,
260            'nofollow' => $this->nofollow,
261            'replacement' => $this->replacement
262        ];
263    }
264}
265