toolbar.module 19.7 KB
Newer Older
1
2
3
4
5
6
7
<?php

/**
 * @file
 * Administration toolbar for quick access to top level administration items.
 */

8
use Drupal\Core\Language\Language;
9
10
use Symfony\Component\HttpFoundation\JsonResponse;
use Drupal\Core\Template\Attribute;
11
use Drupal\Component\Utility\Crypt;
12
use Symfony\Component\HttpFoundation\Response;
13

14
15
16
17
18
19
20
/**
 * Implements hook_help().
 */
function toolbar_help($path, $arg) {
  switch ($path) {
    case 'admin/help#toolbar':
      $output = '<h3>' . t('About') . '</h3>';
21
      $output .= '<p>' . t('The Toolbar module displays links to top-level administration menu items and links from other modules at the top of the screen. For more information, see the online handbook entry for <a href="@toolbar">Toolbar module</a>.', array('@toolbar' => 'http://drupal.org/documentation/modules/toolbar')) . '</p>';
22
23
24
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Displaying administrative links') . '</dt>';
25
      $output .= '<dd>' . t('The Toolbar module displays a bar containing top-level administrative components across the top of the screen. Below that, the Toolbar module has a <em>drawer</em> section where it displays links provided by other modules, such as the core <a href="@shortcuts-help">Shortcut module</a>. The drawer can be hidden/shown by clicking on its corresponding tab.', array('@shortcuts-help' => url('admin/help/shortcut'))) . '</dd>';
26
27
28
29
30
      $output .= '</dl>';
      return $output;
  }
}

31
/**
32
 * Implements hook_permission().
33
 */
34
function toolbar_permission() {
35
36
  return array(
    'access toolbar' => array(
37
      'title' => t('Use the administration toolbar'),
38
39
40
41
42
    ),
  );
}

/**
43
 * Implements hook_theme().
44
45
46
 */
function toolbar_theme($existing, $type, $theme, $path) {
  $items['toolbar'] = array(
47
48
49
50
51
52
53
54
55
56
57
58
59
    'render element' => 'element',
  );
  $items['toolbar_item'] = array(
    'render element' => 'element',
  );
  $items['toolbar_tab_wrapper'] = array(
    'render element' => 'element',
  );
  $items['toolbar_tray_wrapper'] = array(
    'render element' => 'element',
  );
  $items['toolbar_tray_heading_wrapper'] = array(
    'render element' => 'element',
60
  );
61
62
63
  return $items;
}

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
 * Implements hook_element_info().
 */
function toolbar_element_info() {
  $elements = array();

  $elements['toolbar'] = array(
    '#pre_render' => array('toolbar_pre_render'),
    '#theme' => 'toolbar',
    '#attached' => array(
      'library' => array(
        array('toolbar', 'toolbar'),
      ),
    ),
    // Metadata for the toolbar wrapping element.
    '#attributes' => array(
      // The id cannot be simply "toolbar" or it will clash with the simpletest
      // tests listing which produces a checkbox with attribute id="toolbar"
      'id' => 'toolbar-administration',
83
      'class' => array('toolbar',),
84
85
86
87
88
89
90
      'role' => 'navigation',
    ),
    // Metadata for the administration bar.
    '#bar' => array(
      '#heading' => t('Toolbar items'),
      '#attributes' => array(
        'id' => 'toolbar-bar',
91
        'class' => array('toolbar-bar', 'clearfix',),
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
      ),
    ),
  );

  // A toolbar item is wrapped in markup for common styling.  The 'tray'
  // property contains a renderable array. theme_toolbar_tab() is a light
  // wrapper around the l() function. The contents of tray are rendered in
  // theme_toolbar_tab().
  $elements['toolbar_item'] = array(
    '#pre_render' => array('toolbar_pre_render_item'),
    '#theme' => 'toolbar_item',
    '#theme_wrappers' => array('toolbar_tab_wrapper'),
    'tab' => array(
      '#type' => 'link',
      '#title' => NULL,
      '#href' => '',
    ),
  );
  return $elements;
}

113
/**
114
 * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
115
 *
116
117
118
119
120
 * This gets invoked after full bootstrap, so must duplicate some of what's
 * done by _drupal_bootstrap_page_cache().
 *
 * @todo Replace this hack with something better integrated with DrupalKernel
 *   once Drupal's page caching itself is properly integrated.
121
 */
122
123
124
125
126
127
function _toolbar_initialize_page_cache() {
  $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
  drupal_page_is_cacheable(TRUE);

  // If we have a cache, serve it.
  // @see _drupal_bootstrap_page_cache()
128
129
  $request = \Drupal::request();
  $cache = drupal_page_get_cache($request);
130
  if (is_object($cache)) {
131
132
    $response = new Response();
    $response->headers->set('X-Drupal-Cache', 'HIT');
133
134
    date_default_timezone_set(drupal_get_user_timezone());

135
    drupal_serve_page_from_cache($cache, $response, $request);
136

137
138
    $response->prepare($request);
    $response->send();
139
140
141
142
143
    // We are done.
    exit;
  }

  // Otherwise, create a new page response (that will be cached).
144
  drupal_add_http_header('X-Drupal-Cache', 'MISS');
145
146
147
148
149
150
151
152

  // The Expires HTTP header is the heart of the client-side HTTP caching. The
  // additional server-side page cache only takes effect when the client
  // accesses the callback URL again (e.g., after clearing the browser cache or
  // when force-reloading a Drupal page).
  $max_age = 3600 * 24 * 365;
  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
153
154
}

155
/**
156
 * Implements hook_page_build().
157
 *
158
159
 * Add admin toolbar to the page_top region automatically.
 */
160
function toolbar_page_build(&$page) {
161
  $page['page_top']['toolbar'] = array(
162
    '#type' => 'toolbar',
163
164
165
166
167
    '#access' => user_access('access toolbar'),
  );
}

/**
168
 * Builds the Toolbar as a structured array ready for drupal_render().
169
170
171
 *
 * Since building the toolbar takes some time, it is done just prior to
 * rendering to ensure that it is built only if it will be displayed.
172
 *
173
174
175
176
177
178
 * @param array $element
 *  A renderable array.
 *
 * @return
 *  A renderable array.
 *
179
 * @see toolbar_page_build().
180
 */
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
function toolbar_pre_render($element) {

  // Get the configured breakpoints to switch from vertical to horizontal
  // toolbar presentation.
  $breakpoints = entity_load('breakpoint_group', 'module.toolbar.toolbar');
  if (!empty($breakpoints)) {
    $media_queries = array();
    $media_queries['toolbar']['breakpoints'] = array_map(
      function ($object) {
        return $object->mediaQuery;
      },
      $breakpoints->breakpoints
    );

    $element['#attached']['js'][] = array(
      'data' => $media_queries,
      'type' => 'setting',
    );
  }

  // Get toolbar items from all modules that implement hook_toolbar().
202
  $items = Drupal::moduleHandler()->invokeAll('toolbar');
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
  // Allow for altering of hook_toolbar().
  drupal_alter('toolbar', $items);
  // Sort the children.
  uasort($items, 'element_sort');

  // Merge in the original toolbar values.
  $element = array_merge($element, $items);

  // Render the children.
  $element['#children'] = drupal_render_children($element);

  return $element;
}

/**
 * Returns HTML that wraps the administration toolbar.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties and children of
 *     the tray. Properties used: #children, #attributes and #bar.
 */
function theme_toolbar(&$variables) {
  if (!empty($variables['element']['#children'])) {
    $element = $variables['element'];
    $trays = '';
    foreach (element_children($element) as $key) {
      $trays .= drupal_render($element[$key]['tray']);
    }
    return '<nav' . new Attribute($element['#attributes']) . '>'
      . '<div' . new Attribute($element['#bar']['#attributes']) . '>'
234
      . '<h2 class="visually-hidden">' . $element['#bar']['#heading'] . '</h2>'
235
236
      . $element['#children'] . '</div>' . $trays . '</nav>';
  }
237
238
239
}

/**
240
241
242
243
244
245
246
247
248
249
 * Provides markup for associating a tray trigger with a tray element.
 *
 * A tray is a responsive container that wraps renderable content. Trays present
 * content well on small and large screens alike.
 *
 * @param array $element
 *   A renderable array.
 *
 * @return
 *   A renderable array.
250
 */
251
function toolbar_pre_render_item($element) {
252
253
254
255
256
257
258
  // Assign each item a unique ID.
  $id = drupal_html_id('toolbar-item');

  // Provide attributes for a toolbar item.
  $attributes = array(
    'id' => $id,
  );
259
260
261

  // If tray content is present, markup the tray and its associated trigger.
  if (!empty($element['tray'])) {
262
263
264
    // Provide attributes necessary for trays.
    $attributes += array(
      'data-toolbar-tray' => $id . '-tray',
265
266
267
      'aria-owns' => $id,
      'role' => 'button',
      'aria-pressed' => 'false',
268
    );
269

270
271
272
273
274
275
276
    // Merge in module-provided attributes.
    $element['tab'] += array('#attributes' => array());
    $element['tab']['#attributes'] += $attributes;
    $element['tab']['#attributes']['class'][] = 'trigger';

    // Provide attributes for the tray theme wrapper.
    $attributes = array(
277
278
279
      'id' => $id . '-tray',
      'data-toolbar-tray' => $id . '-tray',
      'aria-owned-by' => $id,
280
281
282
283
284
285
    );
    // Merge in module-provided attributes.
    if (!isset($element['tray']['#wrapper_attributes'])) {
      $element['tray']['#wrapper_attributes'] = array();
    }
    $element['tray']['#wrapper_attributes'] += $attributes;
286
    $element['tray']['#wrapper_attributes']['class'][] = 'toolbar-tray';
287
288
289
290
291
292

    if (!isset($element['tray']['#theme_wrappers'])) {
      $element['tray']['#theme_wrappers'] = array();
    }
    // Add the standard theme_wrapper for trays.
    array_unshift($element['tray']['#theme_wrappers'], 'toolbar_tray_wrapper');
293
294
    // Add the theme wrapper for the tray heading.
    array_unshift($element['tray']['#theme_wrappers'], 'toolbar_tray_heading_wrapper');
295
296
297
298
299
300
301
302
303
304
305
306
  }

  return $element;
}

/**
 * Implements template_preprocess_HOOK().
 */
function template_preprocess_toolbar_tab_wrapper(&$variables) {
  if (!isset($variables['element']['#wrapper_attributes'])) {
    $variables['element']['#wrapper_attributes'] = array();
  }
307
  $variables['element']['#wrapper_attributes']['class'][] = 'toolbar-tab';
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
}

/**
 * Returns HTML for a toolbar item.
 *
 * This theme function only renders the tab portion of the toolbar item. The
 * tray portion will be rendered later.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties and children of
 *     the tray. Property used: tab.
 *
 * @see toolbar_pre_render_item().
 * @see theme_toolbar().
 */
function theme_toolbar_item(&$variables) {
  return drupal_render($variables['element']['tab']);
}

/**
 * Returns HTML for wrapping a toolbar tab.
 *
 * Toolbar tabs have a common styling and placement with the toolbar's bar area.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties and children of
 *     the tray. Properties used: #children and #attributes.
 */
function theme_toolbar_tab_wrapper(&$variables) {
  if (!empty($variables['element']['#children'])) {
    $element = $variables['element'];
    return '<div' . new Attribute($element['#wrapper_attributes']) . '>' . $element['#children'] . '</div>';
  }
}

/**
 * Returns HTML for wrapping a toolbar tray.
 *
 * Used in combination with theme_toolbar_tab() to create an
 * association between a link tag in the administration bar and a tray.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties and children of
 *     the tray. Properties used: #children, #toolbar_identifier and
 *     #attributes.
 */
function theme_toolbar_tray_wrapper(&$variables) {
  if (!empty($variables['element']['#children'])) {
    $element = $variables['element'];
360
    return '<div' . new Attribute($element['#wrapper_attributes']) . '><div class="toolbar-lining clearfix">' . $element['#children'] . '</div></div>';
361
362
363
364
365
366
367
368
369
370
371
372
  }
}

/**
 * Returns HTML for prepending a heading to a toolbar tray.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties and children of
 *     the tray. Properties used: #children and #heading.
 */
function theme_toolbar_tray_heading_wrapper(&$variables) {
373
374
375
376
377
378
379
  $element = $variables['element'];
  if (!empty($element['#children'])) {
    $heading = '';
    if (!empty($element['#heading'])) {
      $heading = '<h3 class="toolbar-tray-name visually-hidden">' . $element['#heading'] . '</h3>';
    }
    return $heading . $element['#children'];
380
  }
381
382
}

383
384
385
/**
 * Implements hook_system_info_alter().
 *
386
387
388
389
390
391
 * Indicate that the 'page_top' region (in which the toolbar will be displayed)
 * is an overlay supplemental region that should be refreshed whenever its
 * content is updated.
 *
 * This information is provided for any module that might need to use it, not
 * just the core Overlay module.
392
393
 */
function toolbar_system_info_alter(&$info, $file, $type) {
394
  if ($type == 'theme') {
395
396
397
398
    $info['overlay_supplemental_regions'][] = 'page_top';
  }
}

399
/**
400
 * Implements hook_toolbar().
401
 */
402
403
function toolbar_toolbar() {
  $items = array();
404

405
406
  // The 'Home' tab is a simple link, with no corresponding tray.
  $items['home'] = array(
407
    '#type' => 'toolbar_item',
408
    'tab' => array(
409
410
411
412
413
414
      '#type' => 'link',
      '#title' => t('Home'),
      '#href' => '<front>',
      '#options' => array(
        'attributes' => array(
          'title' => t('Home page'),
415
          'class' => array('toolbar-icon', 'toolbar-icon-home'),
416
        ),
417
      ),
418
    ),
419
    '#weight' => -20,
420
421
  );

422
423
  // Retrieve the administration menu from the database.
  $tree = toolbar_get_menu_tree();
424

425
426
427
428
  // Add attributes to the links before rendering.
  toolbar_menu_navigation_links($tree);

  $menu = array(
429
    '#heading' => t('Administration menu'),
430
431
432
    'toolbar_administration' => array(
      '#type' => 'container',
      '#attributes' => array(
433
        'class' => array('toolbar-menu-administration'),
434
      ),
435
436
437
438
439
440
441
442
443
444
445
      'administration_menu' => menu_tree_output($tree),
    ),
  );

  // To conserve bandwidth, we only include the top-level links in the HTML.
  // The subtrees are included in a JSONP script, cached by the browser. Here we
  // add that JSONP script. We add it as an external script, because it's a
  // Drupal path, not a file available via a stream wrapper.
  // @see toolbar_subtrees_jsonp()
  $menu['toolbar_administration']['#attached']['js'][url('toolbar/subtrees/' . _toolbar_get_subtree_hash())] = array('type' => 'external');

446
447
  // The administration element has a link that is themed to correspond to
  // a toolbar tray. The tray contains the full administrative menu of the site.
448
  $items['administration'] = array(
449
    '#type' => 'toolbar_item',
450
    'tab' => array(
451
452
453
454
455
456
      '#type' => 'link',
      '#title' => t('Menu'),
      '#href' => 'admin',
      '#options' => array(
        'attributes' => array(
          'title' => t('Admin menu'),
457
          'class' => array('toolbar-icon', 'toolbar-icon-menu'),
458
        ),
459
      ),
460
461
    ),
    'tray' => $menu,
462
    '#weight' => -15,
463
464
465
466
467
  );

  return $items;
}

468
/**
469
470
471
472
 * Gets only the top level items below the 'admin' path.
 *
 * @return
 *   An array containing a menu tree of top level items below the 'admin' path.
473
474
 */
function toolbar_get_menu_tree() {
475
  $tree = array();
476
  $query = Drupal::entityQuery('menu_link')
477
478
479
480
481
482
    ->condition('menu_name', 'admin')
    ->condition('module', 'system')
    ->condition('link_path', 'admin');
  $result = $query->execute();
  if (!empty($result)) {
    $admin_link = menu_link_load(reset($result));
483
    $tree = menu_build_tree('admin', array(
484
485
486
487
      'expanded' => array($admin_link['mlid']),
      'min_depth' => $admin_link['depth'] + 1,
      'max_depth' => $admin_link['depth'] + 1,
    ));
488
  }
489

490
491
492
493
  return $tree;
}

/**
494
 * Generates an array of links from a menu tree array.
495
 *
496
497
 * Based on menu_navigation_links(). Adds path based IDs and icon placeholders
 * to the links.
498
499
500
 *
 * @return
 *   An array of links as defined above.
501
 */
502
503
504
505
506
507
508
509
510
511
512
function toolbar_menu_navigation_links(&$tree) {
  foreach ($tree as $key => $item) {
    // Configure sub-items.
    if (!empty($item['below'])) {
      toolbar_menu_navigation_links($tree[$key]['below']);
    }
    // Make sure we have a path specific ID in place, so we can attach icons
    // and behaviors to the items.
    $tree[$key]['link']['localized_options']['attributes'] = array(
      'id' => 'toolbar-link-' . str_replace(array('/', '<', '>'), array('-', '', ''), $item['link']['link_path']),
      'class' => array(
513
514
        'toolbar-icon',
        'toolbar-icon-' . strtolower(str_replace(' ', '-', $item['link']['link_title'])),
515
      ),
516
      'title' => check_plain($item['link']['description']),
517
518
519
    );
  }
}
520

521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
/**
 * Returns the rendered subtree of each top-level toolbar link.
 */
function toolbar_get_rendered_subtrees() {
  $subtrees = array();
  $tree = toolbar_get_menu_tree();
  foreach ($tree as $tree_item) {
    $item = $tree_item['link'];
    if (!$item['hidden'] && $item['access']) {
      if ($item['has_children']) {
        $query = db_select('menu_links');
        $query->addField('menu_links', 'mlid');
        $query->condition('has_children', 1);
        for ($i=1; $i <= $item['depth']; $i++) {
          $query->condition('p' . $i, $item['p' . $i]);
        }
        $parents = $query->execute()->fetchCol();
        $subtree = menu_build_tree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1));
        toolbar_menu_navigation_links($subtree);
        $subtree = menu_tree_output($subtree);
        $subtree = drupal_render($subtree);
542
      }
543
544
545
546
547
548
      else {
        $subtree = '';
      }

      $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['href']);
      $subtrees[$id] = $subtree;
549
550
    }
  }
551
  return $subtrees;
552
553
554
555
556
}

/**
 * Checks whether an item is in the active trail.
 *
557
 * Useful when using a menu generated by menu_tree_all_data() which does
558
559
 * not set the 'in_active_trail' flag on items.
 *
560
561
562
 * @return
 *   TRUE when path is in the active trail, FALSE if not.
 *
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
 * @todo
 *   Look at migrating to a menu system level function.
 */
function toolbar_in_active_trail($path) {
  $active_paths = &drupal_static(__FUNCTION__);

  // Gather active paths.
  if (!isset($active_paths)) {
    $active_paths = array();
    $trail = menu_get_active_trail();
    foreach ($trail as $item) {
      if (!empty($item['href'])) {
        $active_paths[] = $item['href'];
      }
    }
  }
  return in_array($path, $active_paths);
}
581
582
583
584
585

/**
 * Implements hook_library_info().
 */
function toolbar_library_info() {
586
  $libraries['toolbar'] = array(
587
588
589
    'title' => 'Toolbar',
    'version' => VERSION,
    'js' => array(
590
      drupal_get_path('module', 'toolbar') . '/js/toolbar.js' => array(),
591
592
    ),
    'css' => array(
593
      drupal_get_path('module', 'toolbar') . '/css/toolbar.module.css',
594
595
      drupal_get_path('module', 'toolbar') . '/css/toolbar.theme.css',
      drupal_get_path('module', 'toolbar') . '/css/toolbar.icons.css',
596
597
598
599
    ),
    'dependencies' => array(
      array('system', 'jquery'),
      array('system', 'drupal'),
600
      array('system', 'drupalSettings'),
601
      array('system', 'backbone'),
602
603
      array('system', 'matchmedia'),
      array('system', 'jquery.once'),
604
      array('system', 'drupal.displace'),
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
      array('toolbar', 'toolbar.menu'),
    ),
  );

  $libraries['toolbar.menu'] = array(
    'title' => 'Toolbar nested accordion menus.',
    'version' => VERSION,
    'js' => array(
      drupal_get_path('module', 'toolbar') . '/js/toolbar.menu.js' => array(),
    ),
    'css' => array(
      drupal_get_path('module', 'toolbar') . '/css/toolbar.menu.css',
    ),
    'dependencies' => array(
      array('system', 'jquery'),
      array('system', 'drupal'),
621
622
623
624
625
626
      array('system', 'jquery.once'),
    ),
  );

  return $libraries;
}
627
628
629
630
631
632

/**
 * Returns the hash of the per-user rendered toolbar subtrees.
 */
function _toolbar_get_subtree_hash() {
  global $user;
633
  $cid = $user->id() . ':' . language(Language::TYPE_INTERFACE)->id;
634
635
636
637
638
  if ($cache = cache('toolbar')->get($cid)) {
    $hash = $cache->data;
  }
  else {
    $subtrees = toolbar_get_rendered_subtrees();
639
    $hash = Crypt::hashBase64(serialize($subtrees));
640
641
642
643
    cache('toolbar')->set($cid, $hash);
  }
  return $hash;
}