5 * Module to allow Views to be attached as menu items.
7 * This module is a utility module and allows an admin to select a view for a menu item instead of a title and link. When
8 * the link is rendered, the view is inserted instead of the link. In addition, if the
9 * parent item of the menu is a node page, the node id can be passed to the view as an argument using tokens.
11 * Original concept by Randall Knutson - LevelTen Interactive.
12 * Written and maintained by Mark Carver - LevelTen Interactive.
13 * http://www.leveltendesign.com
16 // Include admin form alter hooks.
17 module_load_include('inc', 'menu_views', 'menu_views.admin');
18 //include_once('menu_views.admin.inc');
21 * Implements hook_menu().
23 function menu_views_menu() {
24 // Fake callback, needed for menu item add/edit validation.
25 $items['<view>'] = array(
26 'page callback' => 'drupal_not_found',
27 'access callback' => TRUE,
28 'type' => MENU_CALLBACK,
35 * Implements hook_theme_registry_alter().
36 * Intercepts theme_menu_link().
38 function menu_views_theme_registry_alter(&$registry) {
39 // Save previous value from registry in case another module/theme overwrites theme_menu_link() as well.
40 $registry['menu_views_menu_link_default'] = $registry['menu_link'];
41 $registry['menu_link']['function'] = 'menu_views_menu_link';
42 // Provide Superfish support.
43 if (isset($registry['superfish_menu_item_link'])) {
44 $registry['menu_views_superfish_menu_item_link_default'] = $registry['superfish_menu_item_link'];
45 $registry['superfish_menu_item_link']['function'] = 'menu_views_superfish_menu_item_link';
47 // Provide Responsive Dropdown Menus support.
48 if (isset($registry['responsive_dropdown_menus_item_link'])) {
49 $registry['menu_views_responsive_dropdown_menus_item_link_default'] = $registry['responsive_dropdown_menus_item_link'];
50 $registry['responsive_dropdown_menus_item_link']['function'] = 'menu_views_responsive_dropdown_menus_item_link';
55 * Implements theme_menu_link().
56 * Overrides default theming function to intercept views.
58 function menu_views_menu_link(array $variables) {
59 // Only intercept if this menu link is a view.
60 $view = _menu_views_replace_menu_item($variables['element']);
61 if ($view !== FALSE) {
64 $classes = isset($variables['element']['#attributes']['class']) ? $variables['element']['#attributes']['class'] : array();
65 $item = _menu_views_get_item($variables['element']);
66 foreach (explode(' ', $item['view']['settings']['wrapper_classes']) as $class) {
67 if (!in_array($class, $classes)) {
71 $variables['element']['#attributes']['class'] = $classes;
72 if ($variables['element']['#below']) {
73 $sub_menu = \Drupal::service("renderer")->render($variables['element']['#below']);
75 return '<li' . drupal_attributes($variables['element']['#attributes']) . '>' . $view . $sub_menu . "</li>\n";
79 // Otherwise, use the default theming function.
81 // theme() has been renamed to _theme() and should NEVER be called directly.
82 // Calling _theme() directly can alter the expected output and potentially
83 // introduce security issues (see https://www.drupal.org/node/2195739). You
84 // should use renderable arrays instead.
87 // @see https://www.drupal.org/node/2195739
88 // return theme('menu_views_menu_link_default', $variables);
93 * Implements theme_superfish_menu_item_link().
94 * Overrides default theming function to intercept views.
96 function menu_views_superfish_menu_item_link(array $variables) {
97 // Only intercept if this menu item link is a view.
98 if (isset($variables['menu_item']['link']) && $view = _menu_views_replace_menu_item($variables['menu_item']['link'])) {
99 $item = _menu_views_get_item($variables['menu_item']['link']);
100 return '<div' . drupal_attributes(array('class' => explode(' ', $item['view']['settings']['wrapper_classes']))) . '>' . $view . '</div>';
102 // Otherwise, use the default theming function.
104 // theme() has been renamed to _theme() and should NEVER be called directly.
105 // Calling _theme() directly can alter the expected output and potentially
106 // introduce security issues (see https://www.drupal.org/node/2195739). You
107 // should use renderable arrays instead.
110 // @see https://www.drupal.org/node/2195739
111 // return theme('menu_views_superfish_menu_item_link_default', $variables);
116 * Implements theme_responsive_dropdown_menus_item_link().
117 * Overrides default theming function to intercept views.
119 function menu_views_responsive_dropdown_menus_item_link(array $variables) {
120 // Only intercept if this menu item link is a view.
121 if (isset($variables['menu_item']['link']) && $view = _menu_views_replace_menu_item($variables['menu_item']['link'])) {
122 $item = _menu_views_get_item($variables['menu_item']['link']);
123 return '<div' . drupal_attributes(array('class' => explode(' ', $item['view']['settings']['wrapper_classes']))) . '>' . $view . '</div>';
125 // Otherwise, use the default theming function.
127 // theme() has been renamed to _theme() and should NEVER be called directly.
128 // Calling _theme() directly can alter the expected output and potentially
129 // introduce security issues (see https://www.drupal.org/node/2195739). You
130 // should use renderable arrays instead.
133 // @see https://www.drupal.org/node/2195739
134 // return theme('menu_views_responsive_dropdown_menus_item_link_default', $variables);
139 * Implements hook_menu_breadcrumb_alter().
141 function menu_views_menu_breadcrumb_alter(&$active_trail, $item) {
142 foreach ($active_trail as $key => $parent) {
143 if (isset($parent['link_path']) && $parent['link_path'] == '<view>') {
144 $menu_view = _menu_views_get_item($parent);
145 _menu_views_tokenize($menu_view);
146 // Remove this breadcrumb if the menu item view wants it hidden.
147 if (!(bool)$menu_view['view']['settings']['breadcrumb']) {
148 unset($active_trail[$key]);
151 // Use overridden title if provided.
152 $title = \Drupal\Component\Utility\Xss::filterAdmin($menu_view['view']['settings']['breadcrumb_title']);
153 // Use title provided by view next.
155 $view = views_get_view($menu_view['view']['name']);
156 if ($view && $view->access($menu_view['view']['display']) && $view->set_display($menu_view['view']['display'])) {
157 $title = \Drupal\Component\Utility\Xss::filterAdmin($view->get_title());
161 // If title is still empty, just remove it from the breadcrumb.
163 unset($active_trail[$key]);
166 $active_trail[$key]['title'] = $title;
167 $active_trail[$key]['href'] = $menu_view['view']['settings']['breadcrumb_path'];
174 function _menu_views_replace_menu_item($element) {
176 $item = _menu_views_get_item($element);
177 _menu_views_tokenize($item);
178 if ($item['type'] == 'view' && $item['view']['name'] && $item['view']['display']) {
179 $element['#attributes']['class'][] = 'menu-views';
180 if ($view = views_get_view($item['view']['name'])) {
181 if ($view->access($item['view']['display']) && $view->set_display($item['view']['display'])) {
182 $arguments = explode('/', $item['view']['arguments']);
183 // Need to replace empty arguments with NULL values for views.
184 foreach ($arguments as $key => $value) {
186 $arguments[$key] = NULL;
189 $view->set_arguments($arguments);
190 $build['view'] = array(
191 '#markup' => $view->preview(),
195 // Provide title options for the view.
196 if ((bool)$item['view']['settings']['title']) {
197 $title = \Drupal\Component\Utility\Xss::filterAdmin($item['view']['settings']['title_override']);
199 $title = \Drupal\Component\Utility\Xss::filterAdmin($view->get_title());
201 if (!empty($title)) {
202 $tag = $item['view']['settings']['title_wrapper'];
206 elseif ($tag === '') {
210 $title_attributes = array();
211 if (!empty($item['view']['settings']['title_classes'])) {
212 $title_attributes['class'] = array_filter(explode(' ', $item['view']['settings']['title_classes']));
213 foreach ($title_attributes['class'] as $key => $class) {
214 $title_attributes['class'][$key] = \Drupal\Component\Utility\Html::getClass($class);
217 $build['title'] = array(
218 '#theme' => 'html_tag__menu_views__title',
220 '#attributes' => $title_attributes,
221 '#value' => \Drupal\Component\Utility\Xss::filterAdmin($title),
225 $build['title'] = array(
232 // Add contextual links if allowed and if views_ui module is enabled.
233 if (\Drupal::moduleHandler()->moduleExists('contextual_links') && \Drupal::currentUser()->hasPermission('access contextual links') && \Drupal::moduleHandler()->moduleExists('views_ui')) {
234 views_add_contextual_links($build, 'special_block_-exp', $view, $item['view']['display']);
235 if (!empty($build['#contextual_links'])) {
236 $build['#prefix'] = '<div class="contextual-links-region">';
237 $build['#suffix'] = '</div>';
238 $build['contextual_links'] = array(
239 '#type' => 'contextual_links',
240 '#contextual_links' => $build['#contextual_links'],
241 '#element' => $build,
250 return \Drupal::service("renderer")->render($build);
255 function _menu_views_default_values() {
259 'original_path' => '',
265 'wrapper_classes' => 'menu-views',
266 'breadcrumb' => TRUE,
267 'breadcrumb_title' => '',
268 'breadcrumb_path' => '<front>',
270 'title_wrapper' => '',
271 'title_classes' => '',
272 'title_override' => '',
279 * Helper function to determine whether the form menu item options are a tree.
281 function _menu_views_options_tree($form) {
282 return ((isset($form['#node']) && isset($form['menu']['link']['options']['#tree']) && $form['menu']['link']['options']['#tree']) || (isset($form['options']['#tree']) && $form['options']['#tree']));
286 * Helper function to return the menu view array from a menu item array or form array.
288 function _menu_views_get_item(array &$element = array(), array &$form_state = array()) {
290 $item = $default = _menu_views_default_values();
291 // Remove the type of menu item, will get set afterwards.
292 unset($item['type']);
295 // If $form_state is empty, this is a menu item.
296 if (empty($form_state)) {
297 if (isset($element['menu_views'])) {
298 $provided = &$element['menu_views'];
300 elseif (isset($element['localized_options']['menu_views'])) {
301 $provided = &$element['localized_options']['menu_views'];
303 elseif (isset($element['#localized_options']['menu_views'])) {
304 $provided = &$element['#localized_options']['menu_views'];
306 elseif (isset($element['options']['menu_views'])) {
307 $provided = &$element['options']['menu_views'];
310 // $form_state should be set when used in forms, otherwise this function will not work.
312 $original_element = $element;
313 $values = &$form_state['values'];
314 // Determine if this is a node form.
315 if (isset($element['#node'])) {
316 $element = &$element['menu']['link'];
317 $values = &$values['menu'];
319 // Save the menu item type before proceeding.
320 if (isset($values['menu_item_type'])) {
321 $ajax_type = $values['menu_item_type'];
323 elseif (isset($values['menu']['menu_views']['menu_item_type'])) {
324 $ajax_type = $values['menu']['menu_views']['menu_item_type'];
326 // Save the menu item provided for initial form population.
327 if (isset($element['original_item']['#value']['options']['menu_views'])) {
328 $provided = &$element['original_item']['#value']['options']['menu_views'];
329 if (isset($provided['type'])) {
330 $item['type'] = $provided['type'];
333 elseif (isset($original_element['#node']->menu['options']['menu_views'])) {
334 $provided = &$original_element['#node']->menu['options']['menu_views'];
335 if (isset($provided['type'])) {
336 $item['type'] = $provided['type'];
339 // Determine if the options has a tree value.
340 if (_menu_views_options_tree($original_element)) {
341 $element = &$element['options'];
342 $values = &$values['options'];
344 if (!$provided && isset($element['menu_views'])) {
345 if (isset($element['menu_views']['#value'])) {
346 $provided = $element['menu_views']['#value'];
348 elseif (isset($element['menu_views'])) {
349 $provided = $element['menu_views'];
352 // Allow submitted values to override form values.
353 if (isset($values['menu_views'])) {
354 $provided = $values['menu_views'];
356 if (isset($provided['type']) && isset($item['type'])) {
357 $provided['type'] = $item['type'];
360 // If the menu view element were not set, attempt to determine if this is a form.
361 // By default, the menu view returns default values (no view). If settings were provided by an element or form item, then use those.
363 // Extract available element settings to use for this menu view.
364 foreach (array('mlid', 'original_path', 'view') as $property) {
365 if (isset($provided[$property])) {
366 if (isset($item[$property]) && is_array($item[$property])) {
367 $item[$property] = _menu_views_array_merge_recursive($item[$property], $provided[$property]);
370 $item[$property] = $provided[$property];
375 // Set the type of menu item.
377 $item['type'] = $ajax_type;
379 elseif (isset($provided['type'])) {
380 $item['type'] = $provided['type'];
383 $item['type'] = $default['type'];
385 // Filter out any disabled views.
386 $views = array_keys(views_get_enabled_views());
387 if (!in_array($item['view']['name'], $views)) {
388 $item['view'] = $default['view'];
393 function menu_views_menu_link_alter(&$link) {
394 $item = _menu_views_get_item($link);
395 if ($item['type'] == 'view') {
396 if (isset($link['link_path']) && $link['link_path'] != '<view>') {
397 $item['original_path'] = $link['link_path'];
399 $link['link_path'] = '<view>';
404 * Helper function to return the menu link item based on it's original path.
406 * @param (string) $original_path
407 * The [node] path for which to search for in menu views.
408 * @param (string)|(array) $menu_name
409 * Limit the search of menu view items to the specified menu names.
412 * A keyed array containing the identification integers matching the original path of menu items in the {menu_links} table,
413 * or an empty array if no menu items were found.
415 * @see: menu_views_node_prepare() and menu_views_node_delete().
417 function _menu_views_items_from_original_path($original_path, $menu_name = NULL) {
420 $query = db_select('menu_links', 'm')
421 ->fields('m', array('mlid', 'options'))
422 ->condition('module', 'menu')
423 ->condition('link_path', '<view>');
424 // Set the menu_name condition if present.
425 if (!empty($menu_name)) {
426 $query = $query->condition('menu_name', $menu_name);
428 // Execute the query.
429 $query = $query->execute();
430 // Iterate through all available menu items that are views to match against the original path.
431 while($link = $query->fetchObject()) {
432 if (PHP_VERSION_ID >= 70000) {
433 // FIXME $options = unserialize($link->options, array('allowed_classes' => ['Class1', 'Class2']));
434 $options = unserialize($link->options);
436 $options = unserialize($link->options);
440 $item = _menu_views_get_item($options);
441 if ($item['original_path'] == $original_path) {
442 $mlids[] = $link->mlid;
452 * Implements hook_node_prepare().
454 function menu_views_node_prepare($node) {
455 // Manually call menu's hook_node_prepare() if $node->menu doesn't exist.
456 // @see: drupal.org/node/1878968
457 if (!isset($node->menu) || empty($node->menu)) {
458 menu_node_prepare($node);
460 if (isset($node->nid) && !$node->menu['mlid']) {
461 // Prepare the node for the edit form so that $node->menu always exists.
464 // // The correct configuration object could not be determined. You'll need to
465 // // rewrite this call manually.
466 // $menu_name = strtok(variable_get('menu_parent_' . $node->type, 'main-menu:0'), ':');
470 // Give priority to the default menu.
473 // // The correct configuration object could not be determined. You'll need to
474 // // rewrite this call manually.
475 // $type_menus = variable_get('menu_options_' . $node->type, array('main-menu' => 'main-menu'));
477 if (in_array($menu_name, $type_menus)) {
478 $mlids = _menu_views_items_from_original_path('node/' . $node->nid, $menu_name);
480 // Check all allowed menus if a link does not exist in the default menu.
481 if (empty($mlid) && !empty($type_menus)) {
482 $mlids = _menu_views_items_from_original_path('node/' . $node->nid, array_values($type_menus));
484 // Load the menu link if one was found.
485 $item = empty($mlids) ? array() : menu_link_load(reset($mlids));
486 // Set default values.
491 'menu_name' => $menu_name,
493 'options' => array(),
500 // Set the menu item.
501 $node->menu = _menu_views_array_merge_recursive($default, $item);
502 // Find the depth limit for the parent select.
503 if (!isset($node->menu['parent_depth_limit'])) {
504 $node->menu['parent_depth_limit'] = _menu_parent_depth_limit($node->menu);
510 * Implements hook_node_delete().
512 function menu_views_node_delete(\Drupal\node\NodeInterface $node) {
513 $mlids = _menu_views_items_from_original_path('node/' . $node->id());
514 foreach ($mlids as $mlid) {
515 menu_link_delete($mlid);
520 * Implements hook_node_insert().
522 function menu_views_node_insert(\Drupal\node\NodeInterface $node) {
523 menu_views_node_save($node);
527 * Implements hook_node_presave().
529 function menu_views_node_presave(\Drupal\node\NodeInterface $node) {
530 if (isset($node->menu)) {
531 $link = &$node->menu;
532 $item = _menu_views_get_item($link);
533 // Ensure the enabled property is set.
534 if (!isset($link['enabled'])) {
535 $link['enabled'] = !(bool) $link['hidden'];
537 // If this is a menu view item, override properties on the link so this module handles the save.
538 if ($link['enabled'] && $item['type'] == 'view') {
539 // Save the mlid in the menu_views array so the menu module doesn't delete the link when it detects the mlid.
540 if (!empty($link['mlid'])) {
541 $link['options']['menu_views']['mlid'] = $link['mlid'];
544 // Ensure there is no title so the menu module doesn't try to save this menu item.
545 $link['link_title'] = '';
552 * Implements hook_node_update().
554 function menu_views_node_update(\Drupal\node\NodeInterface $node) {
555 menu_views_node_save($node);
560 * Helper for hook_node_insert() and hook_node_update().
562 function menu_views_node_save(\Drupal\node\NodeInterface $node) {
563 if (isset($node->menu)) {
564 $link = &$node->menu;
565 $item = _menu_views_get_item($link);
566 // Check to see if Menu Views should handle the menu item save.
567 if (!empty($link['enabled']) && $item['type'] == 'view') {
568 // If this an existing menu item, check to see if the mlid was saved in the menu view options array.
569 if (!empty($item['mlid']) && empty($link['mlid'])) {
570 $link['mlid'] = $item['mlid'];
572 // This is a new menu link, create one so we can get the mlid.
573 // Note: This will save the menu link twice on new nodes, which is unavoidable since
574 // we need the mlid to be saved in the menu views options array.
575 elseif (empty($link['mlid'])) {
576 if (!menu_link_save($link)) {
577 drupal_set_message(t('There was an error saving the menu link.'), 'error');
581 // Ensure mlid is properly set.
582 $item['mlid'] = $link['mlid'];
583 // Ensure link_path is properly set.
584 $link['link_path'] = '<view>';
585 // Ensure original_path is properly set.
586 $item['original_path'] = 'node/' . $node->id();
587 // Replace the menu view options in the link and save it.
588 $link['options']['menu_views'] = $item;
589 if (!menu_link_save($link)) {
590 drupal_set_message(t('There was an error saving the menu link.'), 'error');
598 * Implements hook_token_info().
600 function menu_views_token_info() {
601 $tokens['tokens']['menu-link']['node'] = array(
603 'description' => t('The node of the menu link.'),
606 $tokens['tokens']['menu-link']['parent']['node'] = array(
608 'description' => t('The node of the menu link\'s parent.'),
616 * Implements hook_tokens().
618 function menu_views_tokens($type, $tokens, array $data = array(), array $options = array()) {
619 $url_options = array('absolute' => TRUE);
620 if (isset($options['language'])) {
621 $url_options['language'] = $options['language'];
622 $language_code = $options['language']->language;
625 $language_code = NULL;
627 $sanitize = !empty($options['sanitize']);
628 $replacements = array();
630 if ($type == 'menu-link' && !empty($data['menu-link'])) {
631 $link = (array) $data['menu-link'];
632 // menu-link:node tokens.
633 if ($node_tokens = \Drupal::token()->findWithPrefix($tokens, 'node')) {
634 $node = \Drupal::routeMatch()->getParameter('node', 1, $link['link_path']);
635 if (!$node && '<view>' === $link['link_path'] && !empty($link['options']['menu_views']['original_path'])) {
636 $node = \Drupal::routeMatch()->getParameter('node', 1, $link['options']['menu_views']['original_path']);
639 $replacements += \Drupal::token()->generate('node', $node_tokens, array('node' => $node), $options);
642 $replacements += \Drupal::token()->generate('node', $node_tokens, array('node' => NULL), $options);
645 // menu-link:parent tokens.
646 elseif (($parent_tokens = \Drupal::token()->findWithPrefix($tokens, 'parent')) && !empty($link['plid']) && ($parent = menu_link_load($link['plid']))) {
647 $node = \Drupal::routeMatch()->getParameter('node', 1, $parent['link_path']);
648 if (!$node && '<view>' === $parent['link_path'] && !empty($parent['options']['menu_views']['original_path'])) {
649 $node = \Drupal::routeMatch()->getParameter('node', 1, $parent['options']['menu_views']['original_path']);
652 $replacements += \Drupal::token()->generate('node', $parent_tokens, array('node' => $node), $options);
655 $replacements += \Drupal::token()->generate('node', $parent_tokens, array('node' => NULL), $options);
659 return $replacements;
664 * Helper function to tokenize a menu item view arguments and settings.
665 * Alters the menu view item array and the original values are replaced.
667 function _menu_views_tokenize(&$item) {
668 if (isset($item['mlid']) && $item['mlid'] > 0) {
669 $context['menu-link'] = menu_link_load($item['mlid']);
671 'callback' => '_menu_views_tokenize_callback',
673 if (isset($item['view']['arguments']) && !empty($item['view']['arguments'])) {
674 $item['view']['arguments'] = \Drupal::token()->replace($item['view']['arguments'], $context, array_merge($options, array('urlencode' => TRUE, 'clear' => TRUE)));
676 $tokenizable_settings = array('breadcrumb_title', 'breadcrumb_path', 'title_override');
677 if (isset($item['view']['settings'])) {
678 foreach ($item['view']['settings'] as $key => $value) {
679 if (in_array($key, $tokenizable_settings)) {
680 $item['view']['settings'][$key] = \Drupal::token()->replace($value, $context, $options);
689 * Callback for human-readable token value replacements.
691 function _menu_views_tokenize_callback(&$replacements, $data, $options) {
692 foreach ($replacements as $token => $value) {
693 if (isset($options['urlencode']) && $options['urlencode']) {
694 $replacements[$token] = urlencode($value);
701 * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
702 * keys to arrays rather than overwriting the value in the first array with the duplicate
703 * value in the second array, as array_merge does. I.e., with array_merge_recursive,
704 * this happens (documented behavior):
706 * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
707 * => array('key' => array('org value', 'new value'));
709 * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
710 * Matching keys' values in the second array overwrite those in the first array, as is the
711 * case with array_merge, i.e.:
713 * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
714 * => array('key' => 'new value');
716 * Parameters are passed by reference, though only for performance reasons. They're not
717 * altered by this function.
719 * @param array $array1
720 * @param mixed $array2
721 * @author daniel@danielsmedegaardbuus.dk
724 function &_menu_views_array_merge_recursive(array &$array1, &$array2 = NULL) {
726 if (is_array($array2)) {
727 foreach ($array2 as $key => $val) {
728 if (is_array($array2[$key])) {
729 $merged[$key] = isset($merged[$key]) && is_array($merged[$key]) ? _menu_views_array_merge_recursive($merged[$key], $array2[$key]) : $array2[$key];
732 $merged[$key] = $val;