From 43a5235fad56037a31227c90d454080e00f13c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Wed, 3 Oct 2012 16:23:35 -0400 Subject: [PATCH] Issue #1608878 by jessebeach, effulgentsia, sun, tim.plunkett, nod_, dead_arm: Introduce a dropbutton display component to core. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: J. ReneĢe Beach --- core/includes/common.inc | 20 +++ core/includes/theme.inc | 15 ++ core/misc/dropbutton/dropbutton.base-rtl.css | 21 +++ core/misc/dropbutton/dropbutton.base.css | 91 ++++++++++++ core/misc/dropbutton/dropbutton.js | 151 ++++++++++++++++++++ core/misc/dropbutton/dropbutton.theme-rtl.css | 14 ++ core/misc/dropbutton/dropbutton.theme.css | 29 ++++ core/modules/node/node.admin-rtl.css | 7 + core/modules/node/node.admin.css | 24 ++++ core/modules/node/node.admin.inc | 7 +- core/modules/system/system.base.css | 2 +- core/modules/system/system.module | 28 ++++ .../tests/modules/theme_test/theme_test.module | 91 ++++++++++++ core/themes/bartik/css/style.css | 13 ++ core/themes/seven/style.css | 26 ++++ 15 files changed, 536 insertions(+), 3 deletions(-) create mode 100644 core/misc/dropbutton/dropbutton.base-rtl.css create mode 100644 core/misc/dropbutton/dropbutton.base.css create mode 100644 core/misc/dropbutton/dropbutton.js create mode 100644 core/misc/dropbutton/dropbutton.theme-rtl.css create mode 100644 core/misc/dropbutton/dropbutton.theme.css create mode 100644 core/modules/node/node.admin-rtl.css diff --git a/core/includes/common.inc b/core/includes/common.inc index 6879def..ff2fcb3 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -5462,6 +5462,26 @@ function drupal_pre_render_links($element) { } /** + * Pre-render callback: Attaches the dropbutton library and required markup. + */ +function drupal_pre_render_dropbutton($element) { + $element['#attached']['library'][] = array('system', 'drupal.dropbutton'); + $element['#attributes']['class'][] = 'dropbutton'; + if (!isset($element['#theme_wrappers'])) { + $element['#theme_wrappers'] = array(); + } + array_unshift($element['#theme_wrappers'], 'dropbutton_wrapper'); + + // Enable targeted theming of specific dropbuttons (e.g., 'operations' or + // 'operations__node'). + if (isset($element['#subtype'])) { + $element['#theme'] .= '__' . $element['#subtype']; + } + + return $element; +} + +/** * Pre-render callback: Appends contents in #markup to #children. * * This needs to be a #pre_render callback, because eventually assigned diff --git a/core/includes/theme.inc b/core/includes/theme.inc index cb7a84d..20c94c9 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1707,6 +1707,18 @@ function theme_links($variables) { } /** + * Returns HTML for wrapping a dropbutton menu. + * + * @param array $variables + * An associative array containing: + * - element: An associative array containing the properties and children of + * the dropbutton menu. Properties used: #children. + */ +function theme_dropbutton_wrapper($variables) { + return '
' . $variables['element']['#children'] . '
'; +} + +/** * Returns HTML for an image. * * @param $variables @@ -2904,6 +2916,9 @@ function drupal_common_theme() { 'links' => array( 'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array()), ), + 'dropbutton_wrapper' => array( + 'render element' => 'element', + ), 'image' => array( // HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft // allows the alt attribute to be omitted in some cases. Therefore, diff --git a/core/misc/dropbutton/dropbutton.base-rtl.css b/core/misc/dropbutton/dropbutton.base-rtl.css new file mode 100644 index 0000000..044272b --- /dev/null +++ b/core/misc/dropbutton/dropbutton.base-rtl.css @@ -0,0 +1,21 @@ + +/** + * @file + * Base RTL styles for dropbuttons. + */ + +/** + * The dropbutton arrow. + */ +.dropbutton-toggle { + left: 0; + right: auto; +} +.dropbutton-arrow { + left: 0.6667em; + right: auto; +} +.dropbutton-multiple .dropbutton-widget { + padding-left: 2em; + padding-right: 0; +} diff --git a/core/misc/dropbutton/dropbutton.base.css b/core/misc/dropbutton/dropbutton.base.css new file mode 100644 index 0000000..5a5f71c --- /dev/null +++ b/core/misc/dropbutton/dropbutton.base.css @@ -0,0 +1,91 @@ + +/** + * @file + * Base styles for dropbuttons. + */ + +/** + * When a dropbutton has only one option, it is simply a button. + */ +.dropbutton-wrapper, +.dropbutton-wrapper div { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.dropbutton-wrapper { + display: inline-block; +} +.dropbutton-widget { + position: relative; +} +/* UL styles are over-scoped in core, so this selector needs weight parity. */ +.dropbutton-widget .dropbutton { + list-style-image: none; + list-style-type: none; + margin: 0; + padding: 0; +} +.dropbutton li, +.dropbutton a { + display: block; +} + +/** + * The dropbutton styling. + * + * A dropbutton is a widget that displays a list of action links as a button + * with a primary action. Secondary actions are hidden behind a click on a + * twisty arrow. + * + * The arrow is created using border on a zero-width, zero-height span. + * The arrow inherits the link color, but can be overridden with border colors. + */ +.dropbutton-multiple .dropbutton-widget { + padding-right: 2em; /* LTR */ +} +.dropbutton-multiple .dropbutton .secondary-action { + display: none; +} +.dropbutton-multiple.open .dropbutton .secondary-action { + display: block; +} +.dropbutton-toggle { + bottom: 0; + display: block; + position: absolute; + right: 0; /* LTR */ + text-indent: 110%; + top: 0; + white-space: nowrap; + width: 2em; +} +.dropbutton-toggle button { + background: none; + border: 0; + cursor: pointer; + display: block; + height: 100%; + margin: 0; + padding: 0; + width: 100%; +} +.dropbutton-arrow { + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + border-style: solid; + border-width: 0.3333em 0.3333em 0; + display: block; + height: 0; + line-height: 0; + position: absolute; + right: 40%; /* 0.6667em; */ /* LTR */ + top: 0.9em; + width: 0; +} +.dropbutton-multiple.open .dropbutton-arrow { + border-bottom: 0.3333em solid; + border-top-color: transparent; + top: 0.6667em; +} diff --git a/core/misc/dropbutton/dropbutton.js b/core/misc/dropbutton/dropbutton.js new file mode 100644 index 0000000..daaa072 --- /dev/null +++ b/core/misc/dropbutton/dropbutton.js @@ -0,0 +1,151 @@ +(function ($, Drupal) { + +"use strict"; + +/** + * Process elements with the .dropbutton class on page load. + */ +Drupal.behaviors.dropButton = { + attach: function (context, settings) { + var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton'); + if ($dropbuttons.length) { + // Adds the delegated handler that will toggle dropdowns on click. + var $body = $('body').once('dropbutton-click'); + if ($body.length) { + $body.on('click', '.dropbutton-toggle', dropbuttonClickHandler); + } + // Initialize all buttons. + for (var i = 0, il = $dropbuttons.length; i < il; i++) { + DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton)); + } + } + } +}; + +/** + * Delegated callback for opening and closing dropbutton secondary actions. + */ +function dropbuttonClickHandler (e) { + e.preventDefault(); + $(e.target).closest('.dropbutton-wrapper').toggleClass('open'); +} + +/** + * A DropButton presents an HTML list as a button with a primary action. + * + * All secondary actions beyond the first in the list are presented in a + * dropdown list accessible through a toggle arrow associated with the button. + * + * @param {jQuery} $dropbutton + * A jQuery element. + * + * @param {Object} settings + * A list of options including: + * - {String} title: The text inside the toggle link element. This text is + * hidden from visual UAs. + */ +function DropButton (dropbutton, settings) { + // Merge defaults with settings. + var options = $.extend({'title': Drupal.t('List additional actions')}, settings); + var $dropbutton = $(dropbutton); + this.$dropbutton = $dropbutton; + this.$list = $dropbutton.find('.dropbutton'); + // Find actions and mark them. + this.$actions = this.$list.find('li').addClass('dropbutton-action'); + + // Add the special dropdown only if there are hidden actions. + if (this.$actions.length > 1) { + // Identify the first element of the collection. + var $primary = this.$actions.slice(0,1); + // Identify the secondary actions. + var $secondary = this.$actions.slice(1); + $($secondary).addClass('secondary-action'); + // Add toggle link. + $primary.after(Drupal.theme('dropbuttonToggle', options)); + // Bind mouse events. + this.$dropbutton + .addClass('dropbutton-multiple') + .on({ + /** + * Adds a timeout to close the dropdown on mouseleave. + */ + 'mouseleave.dropbutton': $.proxy(this.hoverOut, this), + /** + * Clears timeout when mouseout of the dropdown. + */ + 'mouseenter.dropbutton': $.proxy(this.hoverIn, this) + }); + } +} + +/** + * Extend the DropButton constructor. + */ +$.extend(DropButton, { + /** + * Store all processed DropButtons. + * + * @type {Array} + */ + dropbuttons: [] +}); + +/** + * Extend the DropButton prototype. + */ +$.extend(DropButton.prototype, { + /** + * Toggle the dropbutton open and closed. + * + * @param {Boolean} show + * (optional) Force the dropbutton to open by passing true or to close by + * passing false. + */ + toggle: function (show) { + var isBool = typeof show === 'boolean'; + show = isBool ? show : !this.$dropbutton.hasClass('open'); + this.$dropbutton.toggleClass('open', show); + }, + + hoverIn: function () { + // Clear any previous timer we were using. + if (this.timerID) { + window.clearTimeout(this.timerID); + } + }, + + hoverOut: function () { + // Wait half a second before closing. + this.timerID = window.setTimeout($.proxy(this, 'close'), 500); + }, + + open: function () { + this.toggle(true); + }, + + close: function () { + this.toggle(false); + } +}); + + +$.extend(Drupal.theme, { + /** + * A toggle is an interactive element often bound to a click handler. + * + * @param {Object} options + * - {String} title: (optional) The HTML anchor title attribute and + * text for the inner span element. + * + * @return {String} + * A string representing a DOM fragment. + */ + dropbuttonToggle: function (options) { + return '
  • '; + } +}); + +// Expose constructor in the public space. +Drupal.DropButton = DropButton; + +})(jQuery, Drupal); diff --git a/core/misc/dropbutton/dropbutton.theme-rtl.css b/core/misc/dropbutton/dropbutton.theme-rtl.css new file mode 100644 index 0000000..3e0fd7f --- /dev/null +++ b/core/misc/dropbutton/dropbutton.theme-rtl.css @@ -0,0 +1,14 @@ + +/** + * @file + * General RTL styles for dropbuttons. + */ + +.dropbutton-multiple .dropbutton { + border-left: 1px solid #e8e8e8; + border-right: 0 none; +} +.dropbutton-multiple .dropbutton li > * { + margin-left: 0.25em; + margin-right: 0; +} diff --git a/core/misc/dropbutton/dropbutton.theme.css b/core/misc/dropbutton/dropbutton.theme.css new file mode 100644 index 0000000..041724b --- /dev/null +++ b/core/misc/dropbutton/dropbutton.theme.css @@ -0,0 +1,29 @@ + +/** + * @file + * General styles for dropbuttons. + */ + +.dropbutton-wrapper { + cursor: pointer; +} +.dropbutton-widget { + background-color: white; + border: 1px solid #cccccc; +} +.dropbutton-widget:hover { + border-color: #b8b8b8; +} +.dropbutton .dropbutton-action > * { + padding: 0.1em 0.5em; + white-space: nowrap; +} +.dropbutton .secondary-action { + border-top: 1px solid #e8e8e8; +} +.dropbutton-multiple .dropbutton { + border-right: 1px solid #e8e8e8; /* LTR */ +} +.dropbutton-multiple .dropbutton .dropbutton-action > * { + margin-right: 0.25em; /* LTR */ +} diff --git a/core/modules/node/node.admin-rtl.css b/core/modules/node/node.admin-rtl.css new file mode 100644 index 0000000..d836f65 --- /dev/null +++ b/core/modules/node/node.admin-rtl.css @@ -0,0 +1,7 @@ +/** + * Operations dropbuttons + */ +.dropbutton-widget { + left: 0; + right: auto; +} diff --git a/core/modules/node/node.admin.css b/core/modules/node/node.admin.css index 5777b1f..b2ccf4a 100644 --- a/core/modules/node/node.admin.css +++ b/core/modules/node/node.admin.css @@ -10,3 +10,27 @@ .revision-current { background: #ffc; } + +/** + * Operations dropbuttons + */ +.dropbutton-wrapper { + display: block; + min-height: 2em; + position: relative; +} +.dropbutton-widget { + position: absolute; + right: 0; /* LTR */ +} +.dropbutton-wrapper, +.dropbutton-widget { + max-width: 100%; +} +.dropbutton-multiple.open, +.dropbutton-multiple.open .dropbutton-widget { + max-width: none; +} +.dropbutton-multiple.open { + z-index: 100; +} diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc index b28a190..f77ec7d 100644 --- a/core/modules/node/node.admin.inc +++ b/core/modules/node/node.admin.inc @@ -412,6 +412,9 @@ function node_admin_nodes() { '#title' => t('Update options'), '#attributes' => array('class' => array('container-inline')), '#access' => $admin_access, + '#attached' => array ( + 'css' => array(drupal_get_path('module', 'node') . '/node.admin.css'), + ), ); $options = array(); foreach (module_invoke_all('node_operations') as $operation => $array) { @@ -539,9 +542,9 @@ function node_admin_nodes() { // Render an unordered list of operations links. $options[$node->nid]['operations'] = array( 'data' => array( - '#theme' => 'links__node_operations', + '#type' => 'operations', + '#subtype' => 'node', '#links' => $operations, - '#attributes' => array('class' => array('links', 'inline')), ), ); } diff --git a/core/modules/system/system.base.css b/core/modules/system/system.base.css index 610d94d..c50b085 100644 --- a/core/modules/system/system.base.css +++ b/core/modules/system/system.base.css @@ -1,4 +1,3 @@ - /** * @file * Generic theme-independent base styles. @@ -144,6 +143,7 @@ div.tree-child-horizontal { table.sticky-header { background-color: #fff; margin-top: 0; + z-index: 500; } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 6baa387..d1c314d 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -535,6 +535,14 @@ function system_element_info() { '#default_tab' => '', '#process' => array('form_process_vertical_tabs'), ); + $types['dropbutton'] = array( + '#theme' => 'links__dropbutton', + '#pre_render' => array('drupal_pre_render_dropbutton'), + ); + $types['operations'] = array( + '#theme' => 'links__dropbutton__operations', + '#pre_render' => array('drupal_pre_render_dropbutton'), + ); $types['container'] = array( '#theme_wrappers' => array('container'), @@ -1331,6 +1339,26 @@ function system_library_info() { ), ); + // Dropbutton. + $libraries['drupal.dropbutton'] = array( + 'title' => 'Dropbutton', + 'website' => 'http://drupal.org/node/1608878', + 'version' => '1.0', + 'js' => array( + 'core/misc/dropbutton/dropbutton.js' => array(), + ), + 'css' => array( + 'core/misc/dropbutton/dropbutton.base.css' => array(), + 'core/misc/dropbutton/dropbutton.theme.css' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'drupalSettings'), + array('system', 'jquery.once'), + ), + ); + // Vertical Tabs. $libraries['drupal.vertical-tabs'] = array( 'title' => 'Vertical Tabs', diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index 6187361..a05bc46 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -58,6 +58,19 @@ function theme_test_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + + $items['theme-test'] = array( + 'title' => 'Theme test', + 'page callback' => 'system_admin_menu_block_page', + 'access callback' => TRUE, + 'file path' => drupal_get_path('module', 'system'), + 'file' => 'system.admin.inc', + ); + $items['theme-test/list/operations'] = array( + 'title' => 'Operations', + 'page callback' => '_theme_test_list_operations', + 'access callback' => TRUE, + ); return $items; } @@ -146,3 +159,81 @@ function theme_test_preprocess_html(&$variables) { function theme_theme_test_foo($variables) { return $variables['foo']; } + +/** + * Page callback for manual testing of operation links. + */ +function _theme_test_list_operations() { + $build = array( + '#theme' => 'table', + '#header' => array('Label', 'Created', 'Operations'), + '#rows' => array(), + ); + // Add an item with a very long label, using common operations. + $build['#rows']['long']['label'] = l('An item with a very insanely long label, which offers quite a couple of common operations', 'item/long'); + $build['#rows']['long']['created'] = format_interval(3200); + $build['#rows']['long']['operations'] = array( + 'data' => array( + '#type' => 'operations', + '#subtype' => 'node', + '#links' => array( + 'edit' => array( + 'title' => 'edit', + 'href' => 'item/long/edit', + ), + 'disable' => array( + 'title' => 'disable', + 'href' => 'item/long/disable', + ), + 'clone' => array( + 'title' => 'clone', + 'href' => 'item/long/clone', + ), + 'delete' => array( + 'title' => 'delete', + 'href' => 'item/long/delete', + ), + ), + ), + ); + + // Add another item, using common operations. + $build['#rows']['another']['label'] = l('Another item, using common operations', 'item/another'); + $build['#rows']['another']['created'] = format_interval(8600); + $build['#rows']['another']['operations'] = $build['#rows']['long']['operations']; + + // Add an item with only one operation. + $build['#rows']['one']['label'] = l('An item with only one operation', 'item/one'); + $build['#rows']['one']['created'] = format_interval(12400); + $build['#rows']['one']['operations'] = array( + 'data' => array( + '#type' => 'operations', + '#subtype' => 'node', + '#links' => array( + 'edit' => array( + 'title' => 'edit', + 'href' => 'item/long/edit', + ), + ), + ), + ); + + // Add an item that can only be viewed. + $build['#rows']['view']['label'] = l('An item that can only be viewed', 'item/view'); + $build['#rows']['view']['created'] = format_interval(12400); + $build['#rows']['view']['operations'] = array( + 'data' => array( + '#type' => 'operations', + '#subtype' => 'node', + '#links' => array(), + ), + ); + + // Add an item for which the default operation is denied. + $build['#rows']['denied']['label'] = l('An item for which the default operation is denied', 'item/denied'); + $build['#rows']['denied']['created'] = format_interval(18600); + $build['#rows']['denied']['operations'] = $build['#rows']['long']['operations']; + unset($build['#rows']['denied']['operations']['data']['#links']['edit']); + + return $build; +} diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css index e4b2f12..6f4cb14 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -1608,6 +1608,19 @@ div.admin-panel .description { margin: 0; } +/* ---------- Dropbutton ----------- */ +.dropbutton-widget { + background-color: white; + border-radius: 5px; +} +.dropbutton-widget:hover { + background-color: #f8f8f8; + border-color: #b8b8b8; +} +.dropbutton-multiple.open .dropbutton-widget:hover { + background-color: white; +} + /* ----------- media queries ------------------------------- */ @media all and (min-width: 461px) and (max-width: 900px) { diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css index 24b0035..7d09ddd 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -992,3 +992,29 @@ div.add-or-remove-shortcuts { color: #fff; border-radius: 8px; } + +/* Dropbutton */ +.dropbutton-widget { + background-color: #fff; + background-image: -moz-linear-gradient(-90deg, rgba(255, 255, 255, 0), #e7e7e7); + background-image: -o-linear-gradient(-90deg, rgba(255, 255, 255, 0), #e7e7e7); + background-image: -webkit-linear-gradient(-90deg, rgba(255, 255, 255, 0), #e7e7e7); + background-image: linear-gradient(-90deg, rgba(255, 255, 255, 0), #e7e7e7); + border-radius: 5px; +} +.dropbutton-widget:hover { + background-color: #f0f0f0; + border-color: #b8b8b8; +} +.dropbutton-multiple.open .dropbutton-widget:hover { + background-color: #fff; +} +.dropbutton-content li:first-child > * { + text-overflow: ellipsis; +} +.dropbutton-multiple.open .dropbutton-content li:first-child > * { + text-overflow: clip; +} +.dropbutton-arrow { + right: 35%; +} -- 1.7.10.4