1<?php
2/**
3 * Elgg Menu Item
4 *
5 * To create a menu item that is not a link, pass false for $href.
6 *
7 * Any undocumented properties set will be passed to the output/url view during rendering. E.g.
8 * to give a menu item a "target" attribute, set $item->target, or include a "target" key in
9 * the options array for factory().
10 *
11 * @since 1.8.0
12 */
13class ElggMenuItem implements \Elgg\Collections\CollectionItemInterface {
14
15	/**
16	 * @var array Non-rendered data about the menu item
17	 */
18	protected $data = [
19		// string Identifier of the menu
20		'name' => '',
21
22		// array Page contexts this menu item should appear on
23		'contexts' => ['all'],
24
25		// string Menu section identifier
26		'section' => 'default',
27
28		// int Smaller priorities float to the top
29		'priority' => 100,
30
31		// bool Is this the currently selected menu item (null for autodetection)
32		'selected' => null,
33
34		// string Identifier of this item's parent
35		'parent_name' => '',
36
37		// \ElggMenuItem The parent object or null
38		'parent' => null,
39
40		// array Array of children objects or empty array
41		'children' => [],
42
43		// array An array of options for child menu of the parent item
44		'child_menu' => [],
45
46		// array Classes to apply to the li tag
47		'itemClass' => [],
48
49		// array Classes to apply to the anchor tag
50		'linkClass' => [],
51
52		// array AMD modules required by this menu item
53		'deps' => []
54	];
55
56	/**
57	 * @var string The menu display string (HTML)
58	 */
59	protected $text;
60
61	/**
62	 * @var string The menu url
63	 */
64	protected $href = null;
65
66	/**
67	 * @var string Tooltip
68	 */
69	protected $title = false;
70
71	/**
72	 * @var string The string to display if link is clicked
73	 */
74	protected $confirm = '';
75
76
77	/**
78	 * \ElggMenuItem constructor
79	 *
80	 * @param string $name Identifier of the menu item
81	 * @param string $text Display text of the menu item (HTML)
82	 * @param string $href URL of the menu item (false if not a link)
83	 */
84	public function __construct($name, $text, $href) {
85		$this->text = $text;
86		if ($href) {
87			$this->href = elgg_normalize_url($href);
88		} else {
89			$this->href = $href;
90		}
91
92		$this->data['name'] = $name;
93	}
94
95	/**
96	 * Create an ElggMenuItem from an associative array. Required keys are name, text, and href.
97	 *
98	 * @param array $options Option array of key value pairs
99	 *
100	 *    name        => STR  Menu item identifier (required)
101	 *    text        => STR  Menu item display text as HTML (required)
102	 *    href        => STR  Menu item URL (required)
103	 *                        false = do not create a link.
104	 *                        null = current URL.
105	 *                        "" = current URL.
106	 *                        "/" = site home page.
107	 *                        @warning If href is false, the <a> tag will
108	 *                        not appear, so the link_class will not apply. If you
109	 *                        put <a> tags in manually through the 'text' option
110	 *                        the default CSS selector .elgg-menu-$menu > li > a
111	 *                        may affect formatting. Wrap in a <span> if it does.)
112	 *
113	 *    section     => STR  Menu section identifier
114	 *    link_class  => STR  A class or classes for the <a> tag
115	 *    item_class  => STR  A class or classes for the <li> tag
116	 *    parent_name => STR  Identifier of the parent menu item
117	 *    contexts    => ARR  Page context strings
118	 *    title       => STR  Menu item tooltip
119	 *    selected    => BOOL Is this menu item currently selected?
120	 *    confirm     => STR  If set, the link will be drawn with the output/confirmlink view instead of output/url.
121	 *    deps        => ARR  AMD modules required by this menu item
122	 *    child_menu  => ARR  Options for the child menu
123	 *    data        => ARR  Custom attributes stored in the menu item.
124	 *
125	 * @return ElggMenuItem|null null on error
126	 */
127	public static function factory($options) {
128		if (!isset($options['name']) || !isset($options['text'])) {
129			elgg_log(__METHOD__ . ': $options "name" and "text" are required.', 'ERROR');
130			return null;
131		}
132		if (!isset($options['href'])) {
133			$options['href'] = '';
134		}
135
136		$item = new \ElggMenuItem($options['name'], $options['text'], $options['href']);
137		unset($options['name']);
138		unset($options['text']);
139		unset($options['href']);
140
141		// special catch in case someone uses context rather than contexts
142		if (isset($options['context'])) {
143			$options['contexts'] = $options['context'];
144			unset($options['context']);
145		}
146
147		// make sure contexts is set correctly
148		if (isset($options['contexts'])) {
149			$item->setContext($options['contexts']);
150			unset($options['contexts']);
151		}
152
153		if (isset($options['link_class'])) {
154			$item->setLinkClass($options['link_class']);
155			unset($options['link_class']);
156		}
157
158		if (isset($options['item_class'])) {
159			$item->setItemClass($options['item_class']);
160			unset($options['item_class']);
161		}
162
163		if (isset($options['data']) && is_array($options['data'])) {
164			$item->setData($options['data']);
165			unset($options['data']);
166		}
167
168		foreach ($options as $key => $value) {
169			if (array_key_exists($key, $item->data)) {
170				$item->data[$key] = $value;
171			} else {
172				$item->$key = $value;
173			}
174		}
175
176		return $item;
177	}
178
179	/**
180	 * Set a data key/value pair or a set of key/value pairs
181	 *
182	 * This method allows storage of arbitrary data with this menu item. The
183	 * data can be used for sorting, custom rendering, or any other use.
184	 *
185	 * @param mixed $key   String key or an associative array of key/value pairs
186	 * @param mixed $value The value if $key is a string
187	 * @return void
188	 */
189	public function setData($key, $value = null) {
190		if (is_array($key)) {
191			$this->data = array_merge($this->data, $key);
192		} else {
193			$this->data[$key] = $value;
194		}
195	}
196
197	/**
198	 * Get stored data
199	 *
200	 * @param string $key The key for the requested key/value pair
201	 * @return mixed
202	 */
203	public function getData($key) {
204		if (isset($this->data[$key])) {
205			return $this->data[$key];
206		} else {
207			return null;
208		}
209	}
210
211	/**
212	 * Set the identifier of the menu item
213	 *
214	 * @param string $name Unique identifier
215	 * @return void
216	 */
217	public function setName($name) {
218		$this->data['name'] = $name;
219	}
220
221	/**
222	 * Get the identifier of the menu item
223	 *
224	 * @return string
225	 */
226	public function getName() {
227		return $this->data['name'];
228	}
229
230	/**
231	 * Set the display text of the menu item
232	 *
233	 * @param string $text The display text as HTML
234	 * @return void
235	 */
236	public function setText($text) {
237		$this->text = $text;
238	}
239
240	/**
241	 * Get the display text of the menu item
242	 *
243	 * @return string The display text as HTML
244	 */
245	public function getText() {
246		return $this->text;
247	}
248
249	/**
250	 * Set the URL of the menu item
251	 *
252	 * @param string $href URL or false if not a link
253	 * @return void
254	 * @todo this should probably normalize
255	 */
256	public function setHref($href) {
257		$this->href = $href;
258	}
259
260	/**
261	 * Get the URL of the menu item
262	 *
263	 * @return string|false|null
264	 */
265	public function getHref() {
266		return $this->href;
267	}
268
269	/**
270	 * Set the contexts that this menu item is available for
271	 *
272	 * @param array $contexts An array of context strings. Use 'all' to match all contexts.
273	 * @return void
274	 */
275	public function setContext($contexts) {
276		if (is_string($contexts)) {
277			$contexts = [$contexts];
278		}
279		$this->data['contexts'] = $contexts;
280	}
281
282	/**
283	 * Get an array of context strings
284	 *
285	 * @return array
286	 */
287	public function getContext() {
288		return $this->data['contexts'];
289	}
290
291	/**
292	 * Should this menu item be used given the current context
293	 *
294	 * @param string $context A context string (default is empty string for
295	 *                        current context stack).
296	 * @return bool
297	 */
298	public function inContext($context = '') {
299		if (in_array('all', $this->data['contexts'])) {
300			return true;
301		}
302
303		if ($context) {
304			return in_array($context, $this->data['contexts']);
305		}
306
307		foreach ($this->data['contexts'] as $context) {
308			if (elgg_in_context($context)) {
309				return true;
310			}
311		}
312
313		return false;
314	}
315
316	/**
317	 * Set the selected flag
318	 *
319	 * @param bool $state Selected state (default is true)
320	 * @return void
321	 */
322	public function setSelected($state = true) {
323		$this->data['selected'] = $state;
324	}
325
326	/**
327	 * Get selected state
328	 *
329	 * @return bool
330	 */
331	public function getSelected() {
332		if (isset($this->data['selected'])) {
333			return $this->data['selected'];
334		}
335
336		return elgg_http_url_is_identical(current_page_url(), $this->getHref());
337	}
338
339	/**
340	 * Set the tool tip text
341	 *
342	 * @param string $text The text of the tool tip
343	 * @return void
344	 */
345	public function setTooltip($text) {
346		$this->title = $text;
347	}
348
349	/**
350	 * Get the tool tip text
351	 *
352	 * @return string
353	 */
354	public function getTooltip() {
355		return $this->title;
356	}
357
358	/**
359	 * Set the confirm text shown when link is clicked
360	 *
361	 * @param string $text The text to show
362	 * @return void
363	 */
364	public function setConfirmText($text) {
365		$this->confirm = $text;
366	}
367
368	/**
369	 * Get the confirm text
370	 *
371	 * @return string
372	 */
373	public function getConfirmText() {
374		return $this->confirm;
375	}
376
377	/**
378	 * Set the anchor class
379	 *
380	 * @param mixed $class An array of class names, or a single string class name.
381	 * @return void
382	 */
383	public function setLinkClass($class) {
384		if (!is_array($class)) {
385			$this->data['linkClass'] = [$class];
386		} else {
387			$this->data['linkClass'] = $class;
388		}
389	}
390
391	/**
392	 * Get the anchor classes as text
393	 *
394	 * @return string
395	 */
396	public function getLinkClass() {
397		return implode(' ', $this->data['linkClass']);
398	}
399
400	/**
401	 * Add a link class
402	 *
403	 * @param mixed $class An array of class names, or a single string class name.
404	 * @return void
405	 */
406	public function addLinkClass($class) {
407		$this->addClass($this->data['linkClass'], $class);
408	}
409
410	/**
411	 * Set required AMD modules
412	 *
413	 * @param string[]|string $modules One or more required AMD modules
414	 * @return void
415	 */
416	public function setDeps($modules) {
417		$this->data['deps'] = (array) $modules;
418	}
419
420	/**
421	 * Get required AMD modules
422	 *
423	 * @return string[]
424	 */
425	public function getDeps() {
426		$modules = (array) $this->data['deps'];
427		return array_filter($modules, function($m) {
428			return is_string($m) && !empty($m);
429		});
430	}
431
432	/**
433	 * Add required AMD modules
434	 *
435	 * @param string[]|string $modules One or more required AMD modules
436	 * @return void
437	 */
438	public function addDeps($modules) {
439		$current = $this->getDeps();
440		$this->setDeps($current + (array) $modules);
441	}
442
443	/**
444	 * Set child menu options for a parent item
445	 *
446	 * @param array $options Options
447	 * @return void
448	 */
449	public function setChildMenuOptions(array $options = []) {
450		$this->data['child_menu'] = $options;
451	}
452
453	/**
454	 * Returns child menu options for parent items
455	 *
456	 * @return array
457	 */
458	public function getChildMenuOptions() {
459		return $this->data['child_menu'];
460	}
461
462	/**
463	 * Set the li classes
464	 *
465	 * @param mixed $class An array of class names, or a single string class name.
466	 * @return void
467	 */
468	public function setItemClass($class) {
469		if (!is_array($class)) {
470			$this->data['itemClass'] = [$class];
471		} else {
472			$this->data['itemClass'] = $class;
473		}
474	}
475
476	/**
477	 * Get the li classes as text
478	 *
479	 * @return string
480	 */
481	public function getItemClass() {
482		// allow people to specify name with underscores and colons
483		$name = preg_replace('/[^a-z0-9\-]/i', '-', strtolower($this->getName()));
484
485		$class = implode(' ', $this->data['itemClass']);
486		if ($class) {
487			return "elgg-menu-item-$name $class";
488		} else {
489			return "elgg-menu-item-$name";
490		}
491	}
492
493	/**
494	 * Add a li class
495	 *
496	 * @param mixed $class An array of class names, or a single string class name.
497	 * @return void
498	 * @since 1.9.0
499	 */
500	public function addItemClass($class) {
501		$this->addClass($this->data['itemClass'], $class);
502	}
503
504	// @codingStandardsIgnoreStart
505	/**
506	 * Add additional classes
507	 *
508	 * @param array $current    The current array of classes
509	 * @param mixed $additional Additional classes (either array of string)
510	 * @return void
511	 */
512	protected function addClass(array &$current, $additional) {
513		if (!is_array($additional)) {
514			$current[] = $additional;
515		} else {
516			$current = array_merge($current, $additional);
517		}
518	}
519	// @codingStandardsIgnoreEnd
520
521	/**
522	 * Set the priority of the menu item
523	 *
524	 * @param int $priority The smaller numbers mean higher priority (1 before 100)
525	 * @return void
526	 */
527	public function setPriority(int $priority) {
528		$this->data['priority'] = $priority;
529	}
530
531	/**
532	 * Get the priority of the menu item
533	 *
534	 * @return int
535	 */
536	public function getPriority() {
537		return (int) $this->data['priority'];
538	}
539
540	/**
541	 * Set the section identifier
542	 *
543	 * @param string $section The identifier of the section
544	 * @return void
545	 */
546	public function setSection($section) {
547		$this->data['section'] = $section;
548	}
549
550	/**
551	 * Get the section identifier
552	 *
553	 * @return string
554	 */
555	public function getSection() {
556		return $this->data['section'];
557	}
558
559	/**
560	 * Set the parent identifier
561	 *
562	 * @param string $name The identifier of the parent \ElggMenuItem
563	 * @return void
564	 */
565	public function setParentName($name) {
566		$this->data['parent_name'] = $name;
567	}
568
569	/**
570	 * Get the parent identifier
571	 *
572	 * @return string
573	 */
574	public function getParentName() {
575		return $this->data['parent_name'];
576	}
577
578	/**
579	 * Set the parent menu item
580	 *
581	 * @param \ElggMenuItem $parent The parent of this menu item
582	 * @return void
583	 *
584	 * @internal This is reserved for the \ElggMenuBuilder
585	 */
586	public function setParent($parent) {
587		$this->data['parent'] = $parent;
588	}
589
590	/**
591	 * Get the parent menu item
592	 *
593	 * @return \ElggMenuItem or null
594	 *
595	 * @internal This is reserved for the \ElggMenuBuilder
596	 */
597	public function getParent() {
598		return $this->data['parent'];
599	}
600
601	/**
602	 * Add a child menu item
603	 *
604	 * @param \ElggMenuItem $item A child menu item
605	 * @return void
606	 *
607	 * @internal This is reserved for the \ElggMenuBuilder
608	 */
609	public function addChild($item) {
610		$this->data['children'][] = $item;
611	}
612
613	/**
614	 * Set the menu item's children
615	 *
616	 * @param ElggMenuItem[] $children Array of items
617	 * @return void
618	 *
619	 * @internal This is reserved for the \ElggMenuBuilder
620	 */
621	public function setChildren($children) {
622		$this->data['children'] = $children;
623	}
624
625	/**
626	 * Get the children menu items
627	 *
628	 * @return ElggMenuItem[]
629	 *
630	 * @internal This is reserved for the \ElggMenuBuilder
631	 */
632	public function getChildren() {
633		return $this->data['children'];
634	}
635
636	/**
637	 * Sort the children
638	 *
639	 * @param callable $sortFunction A function that is passed to usort()
640	 *
641	 * @return void
642	 *
643	 * @internal This is reserved for the \ElggMenuBuilder
644	 */
645	public function sortChildren($sortFunction) {
646		foreach ($this->data['children'] as $key => $node) {
647			$node->data['original_order'] = $key;
648			$node->sortChildren($sortFunction);
649		}
650		usort($this->data['children'], $sortFunction);
651	}
652
653	/**
654	 * Get all the values for this menu item. Useful for rendering.
655	 *
656	 * @return array
657	 * @since 1.9.0
658	 */
659	public function getValues() {
660		$values = get_object_vars($this);
661		unset($values['data']);
662
663		return $values;
664	}
665
666	/**
667	 * Get unique item identifier within a collection
668	 * @return string|int
669	 */
670	public function getID() {
671		return $this->getName();
672	}
673}
674