diff --git a/workbench_tabs/.gitignore b/workbench_tabs/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/workbench_tabs/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/workbench_tabs/README.md b/workbench_tabs/README.md new file mode 100644 index 0000000..d5f2484 --- /dev/null +++ b/workbench_tabs/README.md @@ -0,0 +1,9 @@ +# Workbench Tabs + +A module to provide local task tabs and messages as part of the core Toolbar, so that custom themes don't need to provide these administrative elements. + +## Configuration + +* Enable the module +* Grant the "Use Workbench Tabs" permission to one or more roles +* Optionally disable the primary and secondary tabs blocks for the admin theme, so that there are not duplicate task tabs on admin pages diff --git a/workbench_tabs/css/workbench-tabs.css b/workbench_tabs/css/workbench-tabs.css new file mode 100644 index 0000000..25af39c --- /dev/null +++ b/workbench_tabs/css/workbench-tabs.css @@ -0,0 +1,143 @@ +/** + * @file workbench-tabs.css + * CSS for displaying local task tabs below the core toolbar. + */ + +/* Container */ +.workbench-tabs { + background-color: #FFFFFF; +} + +/* Tabs Container */ +.workbench-tabs__tabs { + position: relative; + min-height: 33px; + background-color: #F5F5F5; + box-shadow: 0px 2px 10px #888888 inset; +} + +/* Tabs lists */ +.workbench-tabs__tabs > ul { + list-style: none; + margin: 0; + padding: 0; + text-align: right; +} + +/* Tabs Lists Items */ +.workbench-tabs__tabs > ul > li { + display: inline-block; +} + +/* Primary Tabs default */ +.primary-tabs > li > a { + display: inline-block; + padding: 10px 25px; +} + +/* Primary Tabs hover and focus */ +.primary-tabs > li > a:active, +.primary-tabs > li > a:hover, +.primary-tabs > li > a:focus { + background-color: #FFFFFF; + text-decoration: none; + color: #0032a0; /*Drupal Link color*/ + transition: all 0.15s ease-out; +} + +/* Primary Tabs Active */ +.primary-tabs > li > a.is-active { + background-color: #FFFFFF; + color: #0C2240; /*Drupal hover color*/ + box-shadow: 2px 2px 2px #888888; +} + +/* Secondary Tabs List (ul) */ +.secondary-tabs { + display: block; + border-top: 1px solid #b5b5b5; + background-color: #EBEBEB; + box-shadow: 0px 2px 10px rgba(181, 181, 181, 0.5) inset; +} + +/* Secondary Tabs default link */ +.secondary-tabs > li > a { + display: inline-block; + padding: 5px 15px; +} + +/* Secondary Tabs hover */ +.secondary-tabs > li > a:active, +.secondary-tabs > li > a:hover, +.secondary-tabs > li > a:focus { + background-color: rgba(181, 181, 181, 0.5); + color: #0032a0; /*Drupal Link color*/ +} + +/* Secondary Tabs Active */ +.secondary-tabs > li > a.is-active { + background-color: rgba(181, 181, 181, 0.5); + color: #0C2240; /*Drupal hover color*/ +} + +/* Messages Container */ +.workbench-tabs__message { + padding: 10px 20px; +} + +/* Messages Container, closed */ +.workbench-tabs__message.is-closed { + display: none; + padding: 10px 20px; +} + +/* Zap toolbar link styling within the message area. */ +.workbench-tabs.toolbar .workbench-tabs__message a { + display: inline; + line-height: inherit; +} + +/* Open/Close Messages Trigger */ +.workbench-tabs__trigger { + position: absolute; + top: 0; + left: 20px; + display: inline-block; + border-left: 1px solid #b5b5b5; + border-right: 1px solid #b5b5b5; + padding: 8px 25px; + color: #0032a0; +} + +/* Place "Hide" in for messaging when the drawer is open */ +.workbench-tabs__trigger > .show { + display: none; +} +.workbench-tabs__trigger > .hide { + display: inline; +} + +/* Place "Show" in for messaging when the drawer is closed */ +.workbench-tabs__trigger.is-closed > .show { + display: inline; +} +.workbench-tabs__trigger.is-closed > .hide { + display: none; +} + +/* add in an arrow indicator to the message */ +.workbench-tabs__trigger:after { + content: url('../images/icon-arrow.png'); + position: relative; + top: 2px; + display: inline-block; + margin-left: 10px; +} + +/* Rotate the arrow indicator when the drawer is closed */ +.workbench-tabs__trigger.is-closed:after { + -ms-transform: rotate(180deg); /* IE 9 */ + -webkit-transform: rotate(180deg); /* Chrome, Safari, Opera */ + transform: rotate(180deg); + top: -.5px; +} diff --git a/workbench_tabs/images/icon-arrow.png b/workbench_tabs/images/icon-arrow.png new file mode 100644 index 0000000..8c88c58 Binary files /dev/null and b/workbench_tabs/images/icon-arrow.png differ diff --git a/workbench_tabs/js/workbench_tabs_trigger.js b/workbench_tabs/js/workbench_tabs_trigger.js new file mode 100644 index 0000000..905878a --- /dev/null +++ b/workbench_tabs/js/workbench_tabs_trigger.js @@ -0,0 +1,92 @@ +/** + * @file + * Collapse and expand the workbench_tabs messages. + */ + +(function ($) { + + 'use strict'; + + var $messageTrigger = $('.workbench-tabs__trigger'); + var $messageContents = $('.workbench-tabs__message'); + + var messageHeight = $messageContents.outerHeight(true); + var messagesOpen = messageHeight > 0; + + Drupal.behaviors.workbenchTabs = {}; + + Drupal.behaviors.workbenchTabs.attach = function() { + // Open/Close functionality for rail navigation. + $messageTrigger.once('workbenchTabsMessagesButtonClick').click(function(e) { + e.preventDefault(); + Drupal.behaviors.workbenchTabs.toggleMessagesVisual(); + }); + + // Lose focus on the trigger when the mouse leaves. Using .blur() in the + // click handler breaks menus for users who are tabbing through. + $messageTrigger.once('workbenchTabsMessagesButtonMouseout').mouseout(function() { + $(this).blur(); + }); + + // Close the drawer when we scroll past it. + $(window).once('workbenchTabsMessagesScroll').on('scroll', function() { + if (messagesOpen && $(window).scrollTop() > messageHeight) { + + // Reevaluate message height because user interactions can change it, + // but we don't want to calculate this on every scroll event. + messageHeight = $messageContents.outerHeight(true); + if ($(window).scrollTop() > messageHeight) { + Drupal.behaviors.workbenchTabs.closeMessages(); + + // Scroll to the top of the page to prevent a jump. + $(window).scrollTop(0); + } + } + }); + }; + + Drupal.behaviors.workbenchTabs.toggleMessagesVisual = function() { + $messageContents.slideToggle('slow', function() { + messagesOpen = $messageContents.is(':visible'); + Drupal.behaviors.workbenchTabs.toggleMessages(messagesOpen); + }); + } + + /** + * @param bool state + * Force opening or closing the messages. + * - true: open messages + * - false: close messages + */ + Drupal.behaviors.workbenchTabs.toggleMessages = function(state) { + if (state === true || state === false) { + messagesOpen = !state; + } + else { + messagesOpen = $messageContents.is(':visible'); + } + + if (messagesOpen) { + Drupal.behaviors.workbenchTabs.closeMessages(); + } + else { + Drupal.behaviors.workbenchTabs.openMessages(); + } + } + + Drupal.behaviors.workbenchTabs.closeMessages = function() { + $messageTrigger.addClass('is-closed'); + $messageContents.addClass('is-closed'); + messagesOpen = false; + } + + Drupal.behaviors.workbenchTabs.openMessages = function() { + $messageTrigger.removeClass('is-closed'); + $messageContents + .removeClass('is-closed') + .attr('style', ''); + messageHeight = $messageContents.outerHeight(true); + messagesOpen = true; + } + +})(jQuery); diff --git a/workbench_tabs/src/Element/LocalTasks.php b/workbench_tabs/src/Element/LocalTasks.php new file mode 100644 index 0000000..061e42c --- /dev/null +++ b/workbench_tabs/src/Element/LocalTasks.php @@ -0,0 +1,59 @@ + [[$class, 'preRenderLocalTasks']], + '#theme' => 'workbench_tabs_menu_local_tasks', + '#primary' => '', + '#secondary' => '', + ]; + } + + /** + * @param array $element + * A renderable array. + * + * @return array + * A renderable array. + */ + public static function preRenderLocalTasks($element) { + // This is a workaround for entity pages whose routes have been taken over by + // Page Manager, which breaks local tasks by messing with route names. + $route_name = \Drupal::routeMatch()->getRouteName(); + if (preg_match('/^entity\.[^\.]+\.canonical/', $route_name, $matches)) { + $route_name = $matches[0]; + } + + /** @var \Drupal\Core\Menu\LocalTaskManagerInterface $manager */ + $manager = \Drupal::service('plugin.manager.menu.local_task'); + + foreach (['#primary', '#secondary'] as $i => $key) { + $tabs = $manager->getLocalTasks($route_name, $i); + + foreach (Element::getVisibleChildren($tabs['tabs']) as $tab_key) { + $element[$key][$tab_key] = $tabs['tabs'][$tab_key]; + $element[$key][$tab_key]['#theme'] = 'workbench_tabs_menu_local_task'; + } + } + + return $element; + } + +} diff --git a/workbench_tabs/src/Element/StatusMessages.php b/workbench_tabs/src/Element/StatusMessages.php new file mode 100644 index 0000000..c238c53 --- /dev/null +++ b/workbench_tabs/src/Element/StatusMessages.php @@ -0,0 +1,53 @@ + [[$class, 'renderMessages']], + '#message_type' => NULL, + '#clear_queue' => TRUE, + ]; + } + + /** + * Clear the message queue when rendering the messages. + */ + public static function renderMessages($element) { + $messages = drupal_get_messages($element['#message_type'], $element['#clear_queue']); + + if (!empty($messages)) { + $element = [ + '#theme' => 'workbench_tabs_status_messages', + '#message_list' => $messages, + '#status_headings' => [ + 'status' => t('Status message'), + 'error' => t('Error message'), + 'warning' => t('Warning message'), + ], + ] + $element; + } + + return $element; + } + +} diff --git a/workbench_tabs/templates/workbench-tabs-menu-local-task.html.twig b/workbench_tabs/templates/workbench-tabs-menu-local-task.html.twig new file mode 100644 index 0000000..2b15eaa --- /dev/null +++ b/workbench_tabs/templates/workbench-tabs-menu-local-task.html.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Theme override for a local task link. + * + * This is a copy of stable/templates/navigation/menu-local-task.html.twig + * + * Available variables: + * - attributes: HTML attributes for the wrapper element. + * - is_active: Whether the task item is an active tab. + * - link: A rendered link element. + * + * Note: This template renders the content for each task item in + * menu-local-tasks.html.twig. + * + * @see template_preprocess_menu_local_task() + */ +#} +