From 29963c248d32dd751f39c77b83616af86325eb91 Mon Sep 17 00:00:00 2001
From: camer0n <e107inc@gmail.com>
Date: Sat, 19 Apr 2025 07:53:21 -0700
Subject: [PATCH] Issue #5473 {LINK_CARET} added.

---
 e107_core/templates/admin_template.php       |   4 +-
 e107_handlers/admin_ui.php                   | 360 ++++++++++---------
 e107_handlers/sitelinks_class.php            |  16 +-
 e107_plugins/_blank/admin_config.php         |  18 +-
 e107_themes/bootstrap3/css/modern-dark-2.css |   9 +-
 5 files changed, 220 insertions(+), 187 deletions(-)

diff --git a/e107_core/templates/admin_template.php b/e107_core/templates/admin_template.php
index 48dcd032f..f7a6abbc9 100644
--- a/e107_core/templates/admin_template.php
+++ b/e107_core/templates/admin_template.php
@@ -296,14 +296,14 @@ $ADMIN_TEMPLATE['menu']['start'] = '
 
 $ADMIN_TEMPLATE['menu']['button'] = '
 	<li>
-		<a class="link{LINK_CLASS}" {LINK_DATA} href="{LINK_URL}" {ID}{ONCLICK}><span class="e-tip" data-placement="right" title="{LINK_TEXT}">{LINK_IMAGE}</span><span class="sidebar-toggle-panel"> {LINK_TEXT}{LINK_BADGE}</span></a>
+		<a class="link{LINK_CLASS}" {LINK_DATA} href="{LINK_URL}" {ID}{ONCLICK}><span class="e-tip" data-placement="right" title="{LINK_TEXT}">{LINK_IMAGE}</span><span class="sidebar-toggle-panel"> {LINK_TEXT}{LINK_BADGE}{LINK_CARET}</span></a>
 		{SUB_MENU}
 	</li>
 ';
 
 $ADMIN_TEMPLATE['menu']['button_active'] = '
 	<li class="active">
-		<a class="link-active{LINK_CLASS}" {LINK_DATA} href="{LINK_URL}" {ID}{ONCLICK}><span class="e-tip" data-placement="right" title="{LINK_TEXT}">{LINK_IMAGE}</span><span class="sidebar-toggle-panel"> {LINK_TEXT}{LINK_BADGE}</span></a>
+		<a class="link-active{LINK_CLASS}" {LINK_DATA} href="{LINK_URL}" {ID}{ONCLICK}><span class="e-tip" data-placement="right" title="{LINK_TEXT}">{LINK_IMAGE}</span><span class="sidebar-toggle-panel"> {LINK_TEXT}{LINK_BADGE}{LINK_CARET}</span></a>
 		{SUB_MENU}
 	</li>
 ';
diff --git a/e107_handlers/admin_ui.php b/e107_handlers/admin_ui.php
index 9dda2114e..a50835827 100755
--- a/e107_handlers/admin_ui.php
+++ b/e107_handlers/admin_ui.php
@@ -1566,210 +1566,229 @@ class e_admin_dispatcher
 		return 'e_admin_controller';
 	}
 
+
 	/**
 	 * Generic Admin Menu Generator
+	 *
 	 * @return string
 	 */
-public function renderMenu()
-{
-		
-    $tp = e107::getParser();
-    $var = array();
-    $selected = false;
+	public function renderMenu()
+	{
 
-    foreach ($this->adminMenu as $key => $val)
-    {
+		$tp = e107::getParser();
+		$var = array();
+		$selected = false;
 
-        if (isset($val['perm']) && $val['perm'] !== '' && !getperms($val['perm']))
-        {
-            continue;
-        }
+		foreach($this->adminMenu as $key => $val)
+		{
+			if(isset($val['perm']) && $val['perm'] !== '' && !getperms($val['perm']))
+			{
+				continue;
+			}
 
-        $tmp = explode('/', trim($key, '/'), 3);
-        $isSubItem = count($tmp) === 3;
+			$tmp = explode('/', trim($key, '/'), 2);
+			$isSubItem = !empty($val['group']);
 
-        if ($isSubItem)
-        {
-            $parentKey = $tmp[0].'/'.$tmp[1];
-            if (!$this->hasModeAccess($tmp[0]) || !$this->hasRouteAccess($parentKey))
-            {
-                continue;
-            }
-        }
-        else
-        {
-            if (!$this->hasModeAccess($tmp[0]) || !$this->hasRouteAccess($tmp[0].'/'.varset($tmp[1])))
-            {
-                continue;
-            }
-        }
+			if($isSubItem)
+			{
+				$parentKey = $val['group'];
+				if(!$this->hasModeAccess($tmp[0]) || !$this->hasRouteAccess($parentKey))
+				{
+					continue;
+				}
+			}
+			else
+			{
+				if(!$this->hasModeAccess($tmp[0]) || !$this->hasRouteAccess($key))
+				{
+					continue;
+				}
+			}
 
-        if (isset($val['selected']) && $val['selected'])
-        {
-            $selected = $val['selected'] === true ? $key : $val['selected'];
-        }
+			if(isset($val['selected']) && $val['selected'])
+			{
+				$selected = $val['selected'] === true ? $key : $val['selected'];
+			}
 
-        $processedItem = $this->processMenuItem($val, $key, $tmp);
-        $processedItem['link_id'] = str_replace('/', '-', $key);
+			$processedItem = $this->processMenuItem($val, $key, $tmp);
+			$processedItem['link_id'] = str_replace('/', '-', $key);
 
-        if ($isSubItem)
-        {
-            $parentKey = $tmp[0].'/'.$tmp[1];
-            if (!isset($var[$parentKey]))
-            {
-                $var[$parentKey] = array(
-                    'text' => 'Unknown',
-                    'image_src' => e_navigation::guessMenuIcon($parentKey),
-                    'link_id' => str_replace('/', '-', $parentKey) // Add link_id for parent
-                );
-            }
-            $var[$parentKey]['sub'][$tmp[2]] = $processedItem;
-        }
-        else
-        {
-            $var[$key] = $processedItem;
-        }
-    }
+			if($isSubItem)
+			{
+				if(!isset($var[$parentKey]))
+				{
+					$var[$parentKey] = array(
+						'text'      => 'Unknown',
+						'image_src' => e_navigation::guessMenuIcon($parentKey),
+						'link_id'   => str_replace('/', '-', $parentKey)
+					);
+				}
+				$subKey = str_replace($parentKey . '/', '', $key);
+				$var[$parentKey]['sub'][$subKey] = $processedItem;
+			}
+			else
+			{
+				$var[$key] = $processedItem;
+			}
+		}
 
-    // Handle links and collapse attributes
-    foreach ($var as $key => &$item)
-    {
-        if (!empty($item['sub']))
-        {
-            $item['link'] = '#';
-            $item['link_data'] = [
-                'data-toggle' => 'collapse',
-                'data-target' => '#sub-' . $item['link_id'],
-                'role' => 'button'
-            ];
-            if ($selected === $key || strpos($selected, $key . '/') === 0)
-            {
-                $item['link_data']['aria-expanded'] = 'true';
-            }
-        }
-        elseif (!isset($item['link']))
-        {
-            $tmp = explode('/', trim($key, '/'), 3);
-            $item['link'] = e_REQUEST_SELF.'?mode='.$tmp[0].'&action='.($tmp[1] ?? 'main');
-        }
-    }
+		// Handle links and collapse attributes
+		foreach($var as $key => &$item)
+		{
+			if(!empty($item['sub']))
+			{
+				$item['link'] = '#';
+				$item['link_caret'] = true;
+				$item['link_data'] = [
+					'data-toggle' => 'collapse',
+					'data-target' => '#sub-' . $item['link_id'],
+					'role'        => 'button'
+				];
+				$item['caret'] = true; // Indicate caret for sub-menu parents
 
-    if (empty($var))
-    {
-        return '';
-    }
+				// Check if any sub-item is active to expand the parent
+				$isSubItemActive = false;
+				foreach($item['sub'] as $subKey => &$subItem)
+				{
+					$fullSubPath = $key;
+					if($selected === $key)
+					{
+						$subItem['selected'] = $subKey; // Mark sub-item as active
+						$isSubItemActive = true;
+					}
+				}
 
-    // Debug $var
-    e107::getMessage()->addInfo(print_a($var, true));
+				// Expand the parent if a sub-item is active or if the parent itself is selected
+				if($selected === $key || $isSubItemActive || strpos($selected, $key . '/') === 0)
+				{
+					$item['link_data']['aria-expanded'] = 'true';
+				}
+			}
+			elseif(!isset($item['link']))
+			{
+				$tmp = explode('/', trim($key, '/'), 3);
+				$item['link'] = e_REQUEST_SELF . '?mode=' . $tmp[0] . '&action=' . ($tmp[1] ?? 'main');
+			}
+		}
 
-    $request = $this->getRequest();
-    if (!$selected)
-    {
-        $selected = $request->getMode() . '/' . $request->getAction();
-        if (isset($_GET['sub']) && !empty($_GET['sub']))
-        {
-            $selected .= '/' . $_GET['sub'];
-        }
-    }
-    $selected = vartrue($this->adminMenuAliases[$selected], $selected);
+		if(empty($var))
+		{
+			return '';
+		}
 
-    $icon = '';
+		$request = $this->getRequest();
+		if(!$selected)
+		{
+			$selected = $request->getMode() . '/' . $request->getAction();
+		}
 
-    if (!empty($this->adminMenuIcon))
-    {
-        $icon = e107::getParser()->toIcon($this->adminMenuIcon);
-    }
-    elseif (deftrue('e_CURRENT_PLUGIN'))
-    {
-        $icon = e107::getPlug()->load(e_CURRENT_PLUGIN)->getIcon(24);
-    }
+		$selected = vartrue($this->adminMenuAliases[$selected], $selected);
 
-    $toggle = "<span class='e-toggle-sidebar'><!-- --></span>";
+		$icon = '';
 
-    $var['_extras_'] = array('icon' => $icon, 'return' => true);
+		if(!empty($this->adminMenuIcon))
+		{
+			$icon = e107::getParser()->toIcon($this->adminMenuIcon);
+		}
+		elseif(deftrue('e_CURRENT_PLUGIN'))
+		{
+			$icon = e107::getPlug()->load(e_CURRENT_PLUGIN)->getIcon(24);
+		}
 
-    return $toggle . e107::getNav()->admin($this->menuTitle, $selected, $var);
-}
+		$toggle = "<span class='e-toggle-sidebar'><!-- --></span>";
 
-private function processMenuItem($val, $key, $tmp)
-{
-    $tp = e107::getParser();
-    $item = array();
+		$var['_extras_'] = array('icon' => $icon, 'return' => true);
 
-    foreach ($val as $k => $v)
-    {
-        switch ($k)
-        {
-            case 'caption':
-                $k2 = 'text';
-                $v = defset($v, $v);
-                break;
+//e107::getMessage()->addInfo(print_a($var, true));
+		return $toggle . e107::getNav()->admin($this->menuTitle, $selected, $var);
+	}
 
-            case 'url':
-                $k2 = 'link';
-                $qry = (isset($val['query'])) ? $val['query'] : '?mode='.$tmp[0].'&mp;action='.($tmp[1] ?? 'main').(isset($tmp[2]) ? '&sub='.$tmp[2] : '');
-                $v = $tp->replaceConstants($v, 'abs').$qry;
-                break;
+	/**
+	 * @param $val
+	 * @param $key
+	 * @param $tmp
+	 * @return array
+	 */
+	private function processMenuItem($val, $key, $tmp)
+	{
 
-            case 'uri':
-                $k2 = 'link';
-                $v = $tp->replaceConstants($v, 'abs');
-                if (!empty($v) && ($v === e_REQUEST_URI))
-                {
-                    $GLOBALS['selected'] = $key;
-                }
-                break;
+		$tp = e107::getParser();
+		$item = array();
 
-            case 'badge':
-                $k2 = 'badge';
-                $v = (array) $v;
-                break;
+		foreach($val as $k => $v)
+		{
+			switch($k)
+			{
+				case 'caption':
+					$k2 = 'text';
+					$v = defset($v, $v);
+					break;
 
-            case 'icon':
-                $k2 = 'image_src';
-                $v = (string) $v . '.glyph'; // Ensure .glyph suffix
-                break;
+				case 'url':
+					$k2 = 'link';
+					$qry = (isset($val['query'])) ? $val['query'] : '?mode=' . $tmp[0] . '&amp;action=' . ($tmp[1] ?? 'main') . (isset($tmp[2]) ? '&sub=' . $tmp[2] : '');
+					$v = $tp->replaceConstants($v, 'abs') . $qry;
+					break;
 
-            default:
-                $k2 = $k;
-                break;
-        }
+				case 'uri':
+					$k2 = 'link';
+					$v = $tp->replaceConstants($v, 'abs');
+					if(!empty($v) && ($v === e_REQUEST_URI))
+					{
+						$GLOBALS['selected'] = $key;
+					}
+					break;
 
-        $item[$k2] = $v;
-    }
+				case 'badge':
+					$k2 = 'badge';
+					$v = (array) $v;
+					break;
 
-    if (!isset($item['image_src']))
-    {
-        $item['image_src'] = e_navigation::guessMenuIcon($key); // Includes .glyph
-    }
+				case 'icon':
+					$k2 = 'image_src';
+					$v = (string) $v . '.glyph';  // required, even if empty.
+					break;
 
-    if (!vartrue($item['link']))
-    {
-        $item['link'] = e_REQUEST_SELF.'?mode='.$tmp[0].'&amp;action='.($tmp[1] ?? 'main').(isset($tmp[2]) ? '&sub='.$tmp[2] : '');
-    }
+				default:
+					$k2 = $k;
+					break;
+			}
 
-    if (varset($val['tab']))
-    {
-        $item['link'] .= '&amp;tab=' .$val['tab'];
-    }
+			$item[$k2] = $v;
+		}
 
-    if (!empty($val['modal']))
-    {
-        $item['link_class'] = ' e-modal';
-        if (!empty($val['modal-caption']))
-        {
-            $item['link_data'] = array_merge($item['link_data'] ?? [], ['data-modal-caption' => $val['modal-caption']]);
-        }
-    }
+		if(!isset($item['image_src']))
+		{
+			$item['image_src'] = e_navigation::guessMenuIcon($key);
+		}
 
-    if (!empty($val['class']))
-    {
-				$var[$key]['link_class'] ?? '';
-				$var[$key]['link_class'] .= ' '.$val['class'];
-    }
+		if(!vartrue($item['link']))
+		{
+			$item['link'] = e_REQUEST_SELF . '?mode=' . $tmp[0] . '&amp;action=' . ($tmp[1] ?? 'main') . (isset($tmp[2]) ? '&sub=' . $tmp[2] : '');
+		}
+
+		if(varset($val['tab']))
+		{
+			$item['link'] .= '&amp;tab=' . $val['tab'];
+		}
+
+		if(!empty($val['modal']))
+		{
+			$item['link_class'] = ' e-modal';
+			if(!empty($val['modal-caption']))
+			{
+				$item['link_data'] = array_merge($item['link_data'] ?? [], ['data-modal-caption' => $val['modal-caption']]);
+			}
+		}
+
+		if(!empty($val['class']))
+		{
+			$item['link_class'] = ($item['link_class'] ?? '') . ' ' . $val['class'];
+		}
+
+		return $item;
+	}
 
-    return $item;
-}
 
 	/**
 	 * Render Help Text in <ul> format. XXX TODO
@@ -1778,7 +1797,6 @@ private function processMenuItem($val, $key, $tmp)
 	{
 
 
-		
 	}
 
 	
diff --git a/e107_handlers/sitelinks_class.php b/e107_handlers/sitelinks_class.php
index 0ddaeda1c..7980c0d66 100644
--- a/e107_handlers/sitelinks_class.php
+++ b/e107_handlers/sitelinks_class.php
@@ -1399,20 +1399,25 @@ i.e-cat_users-32{ background-position: -555px 0; width: 32px; height: 32px; }
 			$replace = array();
 
 			$rid = str_replace(array(' ', '_'), '-', $act).($id ? "-{$id}" : '');
-			
+
 			//XXX  && !is_numeric($act) ???
 			if (($active_page == (string) $act)
 			|| (str_replace("?", "", e_PAGE.e_QUERY) == str_replace("?", "", $act))
             || e_REQUEST_HTTP === varset($e107_vars[$act]['link'])
 			)
 			{
+
+
 				$temp = isset($tmpl['button_active' . $kpost]) ? $tmpl['button_active' . $kpost] : '';
+
 			}
 			else
 			{
 				$temp = isset($tmpl['button'.$kpost]) ? $tmpl['button'.$kpost] : '';
 			}
 
+
+
    //     e107::getDebug()->log($e107_vars[$act]['link']);
 
 		//	$temp = $tmpl['button'.$kpost];
@@ -1431,8 +1436,9 @@ i.e-cat_users-32{ background-position: -555px 0; width: 32px; height: 32px; }
 				$tmplateKey = 'button_'.$e107_vars[$act]['template'].$kpost;
 				$temp = varset($tmpl[$tmplateKey]);
 			}
-	
+
 			$replace['LINK_ID'] = $e107_vars[$act]['link_id'] ?? $rid;
+			$replace['LINK_CARET'] = !empty($e107_vars[$act]['link_caret']) ? '<i class="caret-icon fa fa-chevron-down fa-2x"></i>' : '';
 			$replace['LINK_TEXT'] = str_replace(" ", "&nbsp;", varset($e107_vars[$act]['text']));
 			$replace['LINK_DESCRIPTION'] = varset($e107_vars[$act]['description']);
 
@@ -1493,7 +1499,7 @@ i.e-cat_users-32{ background-position: -555px 0; width: 32px; height: 32px; }
 			}
 			else 
 			{
-				$START_SUB = isset($tmpl['start_sub']) ? $tmpl['start_sub'] : '';
+				$START_SUB = $tmpl['start_sub'] ?? '';
 			}		
 	
 			if(!empty($e107_vars[$act]['sub']))
@@ -1506,11 +1512,11 @@ i.e-cat_users-32{ background-position: -555px 0; width: 32px; height: 32px; }
 
 				$replace['SUB_MENU']  = $tp->parseTemplate($START_SUB, false, $replace);
 				$replace['SUB_MENU'] .= $this->admin(false, $active_page, $e107_vars[$act]['sub'], $tmpl, true, (isset($e107_vars[$act]['sort']) ? $e107_vars[$act]['sort'] : $sortlist));
-				$replace['SUB_MENU'] .= isset($tmpl['end_sub']) ? $tmpl['end_sub'] : '';
+				$replace['SUB_MENU'] .= $tmpl['end_sub'] ?? '';
 			}
 
 
-			$text .= $tp->simpleParse($temp, $replace); 
+			$text .= $tp->simpleParse($temp, $replace);
 
 		}
 	
diff --git a/e107_plugins/_blank/admin_config.php b/e107_plugins/_blank/admin_config.php
index d374322ae..3ff4058a2 100644
--- a/e107_plugins/_blank/admin_config.php
+++ b/e107_plugins/_blank/admin_config.php
@@ -49,9 +49,8 @@ class plugin_blank_admin extends e_admin_dispatcher
 		'main/create' 		=> array('caption'=> 'LAN_CREATE', 'perm' => '0'),
 		'main/prefs' 		=> array('caption'=> 'Settings', 'perm' => '0', 'icon'=>'fa-cog'),
 		'main/custom'		=> array('caption'=> 'Custom Pages', 'perm' => '0', 'icon'=>'fa-asterisk'),
-		'main/custom/sub1' => array('caption' => 'Custom Page 1', 'perm' => '0', 'icon' => ''),
-        'main/custom/sub2' => array('caption' => 'Custom Page 2', 'perm' => '0', 'icon' => ''),
-
+		'main/custom1'        => array('group'=>'main/custom', 'caption' => 'Custom Page 1', 'perm' => '0', 'icon' => ''),
+        'main/custom2'        => array('group'=>'main/custom', 'caption' => 'Custom Page 2', 'perm' => '0', 'icon' => ''),
 	);
 
 	/**
@@ -299,12 +298,15 @@ class plugin_blank_admin_ui extends e_admin_ui
 		}
 		
 		
-		public function customPage()
+		public function custom1Page()
 		{
-			$ns = e107::getRender();
-			$text = "Hello World!";
-			$ns->tablerender("Hello",$text);	
-			
+			return "Hello World Customer Page 1!";
+
+		}
+
+		public function custom2Page()
+		{
+			return "Hello World Customer Page 2!";
 		}
 	
 		// left-panel help menu area. (replaces e_help.php used in old plugins)	
diff --git a/e107_themes/bootstrap3/css/modern-dark-2.css b/e107_themes/bootstrap3/css/modern-dark-2.css
index b17a341a5..579801815 100644
--- a/e107_themes/bootstrap3/css/modern-dark-2.css
+++ b/e107_themes/bootstrap3/css/modern-dark-2.css
@@ -1571,7 +1571,7 @@ thead th, thead tr, .table > thead > tr > th { border-bottom: 0; border-left:0;
         }
 
 
-div.admin-left-panel .nav-pills > li > a > span > i { font-size:16px; opacity: 0; width:0;  }
+div.admin-left-panel .nav-pills > li > a > span > i:not(.caret-icon) { font-size:16px; opacity: 0; width:0;  }
 div.admin-left-panel .nav-pills > li > a:not(.text-primary):not(.text-success):not(.text-info):not(.text-warning):not(.text-danger):not(.text-muted) > span > i {
   color: rgba(255,255,255,0.5);
 }
@@ -1682,6 +1682,11 @@ body {
 .admin-ui-nav-menu { width:100% !important }
 #admin-ui-nav-menu a { color: rgba(255,255,255,0.5); }
 #admin-ui-nav-menu a:hover { color: rgba(255,255,255,1); }
+#admin-ui-nav-menu span.sidebar-toggle-panel { display: flex;  justify-content: space-between;}
+#admin-ui-nav-menu .caret-icon { font-size:1.2em; margin-top:8px }
+#admin-ui-nav-menu a:hover .caret-icon { color: rgba(255,255,255,1); }
+#admin-ui-nav-menu a:active { background-color: #2E77B6; color:white }
+
 /* Collapsed state */
 .admin-left-panel.admin-left-panel-collapsed {
   width: 60px;
@@ -1859,6 +1864,8 @@ body#admin-menus .admin-left-panel .panel {
 
 }
 
+
+
 .text-primary,.text-primary:hover{color:#337ab7 !important}
 .text-success,.text-success:hover{color:#51a351 !important}
 .text-danger,.text-danger:hover{color:#F86965 !important}