diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 7766952..d68963e 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1691,6 +1691,52 @@ function theme_links($variables) { } /** + * Builds a render array for theme_dropbutton. + */ +function template_preprocess_dropbutton(&$variables) { + $variables['attributes']['class'][] = 'dropbutton'; + $variables['attributes']['id'] = drupal_html_id('dropbutton'); + $variables['element'] = array( + '#theme' => 'links', + '#links' => $variables['links'], + '#attributes' => $variables['attributes'], + '#attached' => array( + 'library' => array( + array('system', 'drupal.dropbutton'), + ), + ), + ); + // Pass through title to the dropbutton options. + if (isset($variables['title'])) { + $variables['element']['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => array('dropbutton' => array('title' => $variables['title'])), + ); + } +} + +/** + * Create a dropbutton menu. + * + * @param $title + * The text to place in the clickable area to activate the dropbutton. This + * text is indented to -9999px by default. + * @param $links + * A list of links to provide within the dropbutton, suitable for use + * in via Drupal's theme('links'). + * @param $image + * If true, the dropbutton link is an image and will not get extra decorations + * that a text dropbutton link will. + * @param $class + * An optional class to add to the dropbutton's container div to allow you + * to style a single dropbutton however you like without interfering with + * other dropbuttons. + */ +function theme_dropbutton($variables) { + return render($variables['element']); +} + +/** * Returns HTML for an image. * * @param $variables @@ -2843,6 +2889,9 @@ function drupal_common_theme() { 'links' => array( 'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array()), ), + 'dropbutton' => array( + 'variables' => array('title' => NULL, 'links' => array(), 'attributes' => array()), + ), '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..64684f5 --- /dev/null +++ b/core/misc/dropbutton/dropbutton.base-rtl.css @@ -0,0 +1,26 @@ +/** + * @file dropbutton.base-rtl.css + */ + +/** + * The dropbutton 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-link { + left: 0; + right: auto; +} +.dropbutton-arrow { + left: 0.6667em; + right: auto; +} +.dropbutton-multiple .dropbutton-widget { + left: auto; + right: 0; +} +.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..5876f6f --- /dev/null +++ b/core/misc/dropbutton/dropbutton.base.css @@ -0,0 +1,100 @@ +/** + * @file dropbutton.base.css + * + * When a dropbutton has only one option, it is simply a button. + */ +.dropbutton-wrapper, +.dropbutton-wrapper * { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.dropbutton-wrapper, +.dropbutton-widget { + max-width: 100%; +} +.dropbutton-wrapper { + min-height: 1em; +} +.dropbutton-widget { + display: inline-block; +} +/* ul styles are over-scoped in core, so this selector needs weight parity */ +.dropbutton-widget .dropbutton-content { + list-style-image: none; + list-style-type: none; + margin: 0; + padding: 0; +} +.dropbutton-content li, +.dropbutton-content 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 { + position: relative; +} +.dropbutton-multiple .dropbutton-widget { + display: block; + padding-right: 2em; /* LTR */ + position: absolute; + left: 0; /* LTR */ +} +.dropbutton-multiple.open, +.dropbutton-multiple.open .dropbutton-widget { + max-width: none; +} +.dropbutton-multiple.open { + z-index: 100; +} +.dropbutton-multiple .dropbutton-content { + overflow: hidden; +} +.dropbutton-link { + bottom: 0; + display: none; + overflow: hidden; + position: absolute; + right: 0; /* LTR */ + text-indent: 110%; + top: 0; + white-space: nowrap; + width: 2em; +} +.dropbutton-multiple .dropbutton-link { + display: block; +} +.dropbutton-multiple .dropbutton-content .secondary-actions { + display: none; +} +.dropbutton-multiple.open .dropbutton-content .secondary-actions { + display: block; +} +.dropbutton-arrow { + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + border-style: solid; + border-width: 0.3333em 0.3333em 0; + /* The dropbutton arrow is only dipslayed if JavaScript is enabled. */ + display: inline-block; + line-height: 0; + position: absolute; + right: 0.6667em; /* LTR */ + top: 0.667em; +} +.dropbutton-multiple.open .dropbutton-arrow { + border-bottom: 0.3333em solid; + border-top-color: transparent; + top: 0.333em; +} diff --git a/core/misc/dropbutton/dropbutton.js b/core/misc/dropbutton/dropbutton.js new file mode 100644 index 0000000..6adf0b2 --- /dev/null +++ b/core/misc/dropbutton/dropbutton.js @@ -0,0 +1,181 @@ +/** + * @file dropbutton.js + * + * See dropbutton.theme.inc for primary documentation. + */ + +(function ($) { + +"use strict"; + +/** + * Process elements with the .dropbutton class on page load. + */ +Drupal.behaviors.dropButton = { + attach: function (context, settings) { + // Merge settings with defaults. + var options = $.extend(defaults, (('dropbutton' in settings) ? settings.dropbutton : {})); + var $dropbuttons = $(context).find('.dropbutton').once('dropbutton'); + for (var i = 0, il = $dropbuttons.length; i < il; i++) { + DropButton.dropbuttons.push(new DropButton($dropbuttons[i], options)); + } + } +}; + +/** + * DropButton defaults. + */ +var defaults = { + title: Drupal.t('Open dropbutton') +}; + +/** + * A DropButton presents an HTML list as a button with a primary action. + * + * Secondary actions - all actions beyond the first in the list - are presented + * in a dropdown list accessible through a toggle arrow associated with the + * button. + * + * @dropbutton {DOMElement}: An HTMLListElement. + * + * @options {Object}: A list of options including: + * - title: The text inside the toggle link element. This text is hidden from + * visual UAs. + */ +function DropButton (dropbutton, options) { + this.dropbutton = dropbutton; + this.$list = $(dropbutton); + // State properties. + this.isOpen = false; + this.isHovered = false; + this.timerID = 0; + // Build the view. + this.build(options); +} + +/** + * Extend the DropButton constructor. + */ +$.extend(DropButton, { + /** + * Store all processed DropButtons. + * + * @type {Array} + */ + dropbuttons: [] +}); + +/** + * Extend the DropButton prototype. + */ +$.extend(DropButton.prototype, { + /** + * Render the dropbutton view. + */ + build: function (options) { + // Save the id and class attributes so that they can be applied to the + // widget wrapper below. + var id = this.$list.attr('id') || ''; + var classes = this.$list.attr('class').split(' ') || []; + classes.push('dropbutton-wrapper'); + // Remove the class and id attributes. The values of these attributes + // will be applied to the dropbutton wrapper. + this.$list.removeAttr('class id'); + // Mark-up the list with dropbutton structure. + this.$list.addClass('dropbutton-content'); + this.$dropbutton = this.$list + .wrap($('
', { + 'class': classes.join(' '), + 'id': id, + 'html': $('
', { + 'class': 'dropbutton-widget' + }) + }) + ) + .closest('.dropbutton-wrapper'); + // If the button has more than one action item, mark it as + // dropbutton-multiple and add a toggle. + // The secondary actions are hidden initially. + this.$secondaryActions = this.$list + .find('li') + .not(':first') + .addClass('secondary-actions'); + if (this.$secondaryActions.length) { + this.$dropbutton.addClass('dropbutton-multiple'); + // Build the dropbutton-toggle. + this.$toggle = $('
', { + 'class': 'dropbutton-link', + 'html': $('', { + 'class': 'dropbutton-arrow', + 'html': '' + options.title + '', + 'href': '#' + }) + }) + .prependTo(this.$dropbutton.children('.dropbutton-widget')); + } + // Bind events. + this.$dropbutton + .on({ + 'mouseenter.dropbutton': $.proxy(this, 'hoverIn'), + 'mouseleave.dropbutton': $.proxy(this, 'hoverOut') + }) + .on({ + 'click.dropbutton': $.proxy(this, 'toggleClickHandler') + }, '.dropbutton-link'); + }, + hoverIn: function () { + // Clear any previous timer we were using. + if (this.timerID) { + clearTimeout(this.timerID); + } + this.isHovered = true; + }, + hoverOut: function () { + this.isHovered = false; + this.toggle(); + }, + toggleClickHandler: function (event) { + event.preventDefault(); + var closeIt = (this.isOpen) ? true : false; + this.toggle(closeIt); + }, + /** + * Toggle the dropbutton open and closed. + * + * @type {Boolean}: Force the dropbutton to close by passing true. + */ + toggle: function (closeIt) { + if (closeIt) { + this.close(); + return; + } + // If the dropbutton is open then prepare to close it. + if (this.isOpen && !this.isHovered) { + // Wait half a second before closing. + this.timerID = setTimeout($.proxy(this, 'close'), 500); + return; + } + if (this.isHovered) { + // Open it. + this.open(); + } + }, + close: function () { + this.isOpen = false; + // this.$secondaryActions.slideUp(100); + this.$dropbutton.removeClass('open'); + }, + open: function () { + this.isOpen = true; + /* this.$secondaryActions.animate({ + height: "show", + opacity: "show" + }, 100); */ + this.$dropbutton.addClass('open'); + } +}); + +// Expose constructor in the public space. +Drupal.DropButton = DropButton; + +})(jQuery); diff --git a/core/misc/dropbutton/dropbutton.theme-rtl.css b/core/misc/dropbutton/dropbutton.theme-rtl.css new file mode 100644 index 0000000..27eb994 --- /dev/null +++ b/core/misc/dropbutton/dropbutton.theme-rtl.css @@ -0,0 +1,8 @@ +/** + * @file dropbutton.theme-rtl.css + */ + +.dropbutton-multiple .dropbutton-content { + border-left: 1px solid #e8e8e8; + border-right: 0 none; +} diff --git a/core/misc/dropbutton/dropbutton.theme.css b/core/misc/dropbutton/dropbutton.theme.css new file mode 100644 index 0000000..78039b8 --- /dev/null +++ b/core/misc/dropbutton/dropbutton.theme.css @@ -0,0 +1,32 @@ +/** + * @file dropbutton.theme.css + */ +.dropbutton-wrapper { + cursor: pointer; + min-height: 2em; +} +.dropbutton-widget { + background-color: white; + border: 1px solid #cccccc; +} +.dropbutton-widget:hover { + border-color: #b8b8b8; +} +.dropbutton-content { + line-height: 1.333; +} +.dropbutton-content li > * { + margin-right: 0.3333em; + overflow: hidden; + padding: 0.25em 0.75em; + white-space: nowrap; +} +.dropbutton-content li + li { + border-top: 1px solid #e8e8e8; +} +.dropbutton-multiple .dropbutton-content { + border-right: 1px solid #e8e8e8; /* LTR */ +} +.dropbutton-arrow:focus { + outline: medium none; +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index e5dc768..b03f647 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1392,6 +1392,25 @@ 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', 'jquery.once'), + array('system', 'drupal'), + ), + ); + // Vertical Tabs. $libraries['drupal.vertical-tabs'] = array( 'title' => 'Vertical Tabs', diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css index 7532a4a..8dfba0b 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -1609,6 +1609,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 ebaa86b..ad66c73 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -964,3 +964,26 @@ 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; +}