<?php
namespace Croogo\Menus\View\Helper;
use Cake\Event\Event;
use Cake\Log\LogTrait;
use Cake\Routing\Exception\MissingRouteException;
use Cake\Routing\Router;
use Cake\Utility\Hash;
use Cake\View\Helper;
use Cake\View\View;
use Croogo\Core\Nav;
use Croogo\Core\Utility\StringConverter;
/**
* Menus Helper
*
* @category Menus.View/Helper
* @package Croogo.Menus.View.Helper
* @version 1.0
* @author Fahad Ibnay Heylaal <contact@fahad19.com>
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link http://www.croogo.org
*/
class MenusHelper extends Helper
{
use LogTrait;
public $helpers = [
'Html',
'Layout',
];
/**
* constructor
*/
public function __construct(View $view, $settings = [])
{
parent::__construct($view, $settings);
$this->_setupEvents();
$this->_converter = new StringConverter();
}
/**
* setup events
*/
protected function _setupEvents()
{
$events = [
'Helper.Layout.beforeFilter' => [
'callable' => 'filter', 'passParams' => true,
],
];
$eventManager = $this->_View->getEventManager();
foreach ($events as $name => $config) {
$eventManager->on($name, $config, [$this, 'filter']);
}
}
/**
* beforeRender
*/
public function beforeRender($viewFile)
{
$request = $this->getView()->getRequest();
if (($request->getParam('prefix') === 'admin') && (!$request->is('ajax'))) {
$this->_adminMenu();
if ($request->getParam('plugin') == 'Croogo/Menus') {
$this->_View->Js->buffer('Links.init();');
}
}
}
/**
* Inject admin menu items
*/
protected function _adminMenu()
{
if (empty($this->_View->viewVars['menus_for_admin_layout'])) {
return;
}
$menus = $this->_View->viewVars['menus_for_admin_layout'];
if (!Nav::check('sidebar', 'menus')) {
return;
}
foreach ($menus as $menu) {
$weight = 9999 + $menu->weight;
$htmlAttributes = $this->__isCurrentMenu($menu->id) ? ['class' => 'current'] : [];
Nav::add('sidebar', 'menus.children.' . $menu->alias, [
'title' => $menu->title,
'url' => [
'prefix' => 'admin',
'plugin' => 'Croogo/Menus',
'controller' => 'Links',
'action' => 'index',
'?' => ['menu_id' => $menu->id]
],
'weight' => $weight,
'htmlAttributes' => $htmlAttributes
]);
};
}
/**
* Checks wether $id is the current active menu
*
* The value is checked against the menuId variable set in
* LinksController::admin_add() and LinksController::admin_edit()
*
* @param string $id Menu id
* @return bool True if $id is currently the active menu
*/
private function __isCurrentMenu($id)
{
$currentMenuId = $this->_View->get('menuId');
return $currentMenuId === $id;
}
/**
* Filter content for Menus
*
* Replaces [menu:menu_alias] or [m:menu_alias] with Menu list
*
* @param Event $event
* @return string
*/
public function filter(Event $event, $options = [])
{
$data = $event->getData();
preg_match_all('/\[(menu|m):([A-Za-z0-9_\-]*)(.*?)\]/i', $data['content'], $tagMatches);
for ($i = 0, $ii = count($tagMatches[1]); $i < $ii; $i++) {
$regex = '/(\S+)=[\'"]?((?:.(?![\'"]?\s+(?:\S+)=|[>\'"]))+.)[\'"]?/i';
preg_match_all($regex, $tagMatches[3][$i], $attributes);
$menuAlias = $tagMatches[2][$i];
$options = [];
for ($j = 0, $jj = count($attributes[0]); $j < $jj; $j++) {
$options[$attributes[1][$j]] = $attributes[2][$j];
}
$options = Hash::expand($options);
$data['content'] = str_replace($tagMatches[0][$i], $this->verticalNav($menuAlias, $options), $data['content']);
}
return $event->getData();
}
/**
* Output simple vertical nav
*/
protected function verticalNav($menuAlias, $options = [])
{
$menu = Hash::get($this->_View->viewVars, "menusForLayout.$menuAlias");
if (!$menu) {
return false;
}
$items = [];
$roleId = $this->Layout->getRoleId();
foreach ($menu['threaded'] as $item) {
if (!empty($item->visibility_roles) && !in_array($roleId, $item->visibility_roles)) {
continue;
}
$url = $item->link->getUrl();
try {
$items[] = $this->Html->link($item->title, $url, [
'class' => 'nav-link',
]);
} catch (MissingRouteException $e) {
$this->log('Cannot normalize url: ' . print_r($url, true), LOG_WARNING);
}
}
if (!$items) {
return null;
}
return $this->Html->tag('nav', implode('', $items), [
'class' => 'nav flex-column',
]);
}
/**
* Show Menu by Alias
*
* @param string $menuAlias Menu alias
* @param array $options (optional)
* @return string
*/
public function menu($menuAlias, $options = [])
{
$_options = [
'tag' => 'ul',
'tagAttributes' => [
'class' => 'dropdown-menu bg-dark',
],
'subTag' => 'li',
'subTagAttributes' => [
'class' => 'nav-item',
],
'linkAttributes' => [
'class' => 'nav-link js-scroll-trigger',
],
'selected' => 'selected',
'dropdown' => false,
'dropdownClass' => 'navbar-nav ml-auto',
'element' => 'Croogo/Menus.menu',
];
$options = array_merge($_options, $options);
if (!isset($this->_View->viewVars['menusForLayout'][$menuAlias])) {
return false;
}
$menu = $this->_View->viewVars['menusForLayout'][$menuAlias];
$output = $this->_View->element($options['element'], [
'menu' => $menu,
'options' => $options,
]);
return $output;
}
/**
* Merge Link options retrieved from Params behavior
*
* @param array $link Link data
* @param string $param Parameter name
* @param array $options Default options
* @return string
*/
protected function _mergeLinkParams($link, $param, $options = [])
{
if (isset($link['Params'][$param])) {
$options = array_merge($options, $link['Params'][$param]);
}
$booleans = ['true', 'false'];
foreach ($options as $key => $val) {
if ($val == null) {
unset($options[$key]);
}
if (is_string($val) && in_array(strtolower($val), $booleans)) {
$options[$key] = ($val === 'true');
}
}
return $options;
}
/**
* Nested Links
*
* @param array $links model output (threaded)
* @param array $options (optional)
* @param int $depth level
* @return string
*/
public function nestedLinks($links, $options = [], $depth = 1)
{
$_options = [
'linkAttributes' => []
];
$options = array_merge($_options, $options);
$roleId = $this->Layout->getRoleId();
$output = '';
foreach ($links as $link) {
$linkAttr = $options['linkAttributes'] + [
'id' => 'link-' . $link->id,
'rel' => $link->rel,
'target' => $link->target,
'title' => $link->description,
'class' => $link->class,
];
if (!empty($link->visibility_roles) && !in_array($roleId, $link->visibility_roles)) {
continue;
}
$linkAttr = $this->_mergeLinkParams($link, 'liAttr', $linkAttr);
if (!empty($link->class) && strpos($linkAttr['class'], $link->class)) {
$linkAttr['class'] = $this->addClass($linkAttr['class'], $link->class);
}
// Remove locale part before comparing links
if ($this->getView()->getRequest()->getParam('locale')) {
$currentUrl = substr($this->getView()->getRequest()->getPath(), strlen($this->getView()->getRequest()->getParam('locale') . '/'));
} else {
$currentUrl = $this->getView()->getRequest()->getPath();
}
try {
if (Router::url($link->link->getUrl()) == Router::url('/' . $currentUrl)) {
if (!isset($linkAttr['class'])) {
$linkAttr['class'] = '';
}
$linkAttr['class'] .= ' ' . $options['selected'];
}
} catch (MissingRouteException $e) {
$this->log(
sprintf(
'MissingRouteException for menu id %d - %s:',
$link->id,
$link->title
),
LOG_WARNING
);
$this->log($e->getMessage(), LOG_WARNING);
continue;
}
if (isset($link['children']) && count($link['children']) > 0) {
$linkAttr['class'] .= ' dropdown-toggle';
$linkAttr['data-toggle'] = 'dropdown';
$linkAttr['aria-haspopup'] = 'true';
$linkAttr['aria-expanded'] = 'false';
}
$linkOutput = $this->Html->link($link->title, $link->link->getUrl(), $linkAttr);
if (isset($link['children']) && count($link['children']) > 0) {
$childOptions = $options;
$childOptions['subTagAttributes']['class'] = 'dropdown-item bg-dark';
$linkOutput .= $this->nestedLinks($link['children'], $childOptions, $depth + 1);
}
$liAttr = $this->_mergeLinkParams($link, 'liAttr');
$liAttr = !empty($liAttr) ? $liAttr : $options['subTagAttributes'];
if (isset($link['children']) && count($link['children']) > 0) {
$liAttr['class'] .= ' dropdown';
}
$linkOutput = $this->Html->tag($options['subTag'], $linkOutput, $liAttr);
$output .= $linkOutput;
}
if ($output != null) {
$tagAttr = $options['tagAttributes'];
if ($options['dropdown'] && $depth == 1) {
$tagAttr['class'] = $options['dropdownClass'];
}
$output = $this->Html->tag($options['tag'], $output, $tagAttr);
}
return $output;
}
/**
* Converts strings like controller:abc/action:xyz/ to arrays
*
* @param string|array $link link
* @return array
* @see Use StringConverter::linkStringToArray()
*/
public function linkStringToArray($link)
{
return $this->_converter->linkStringToArray($link);
}
/**
* Converts array into string controller:abc/action:xyz/value1/value2
*
* @param array $url link
* @return array
* @see StringConverter::urlToLinkString()
*/
public function urlToLinkString($url)
{
return $this->_converter->urlToLinkString($url);
}
}