Commit e888f006 authored by webchick's avatar webchick

#323112 by dmitrig01, kkaefer, quicksketch, frando and many many more: Now...

#323112 by dmitrig01, kkaefer, quicksketch, frando and many many more: Now presenting... Vertical Tabs. Fantastic new UI improvement for node forms and hopefully more in the future.
parent f278da9e
......@@ -3368,6 +3368,28 @@ function drupal_render(&$elements) {
}
}
// Add additional CSS and JavaScript files associated with this element.
foreach (array('css', 'js') as $kind) {
if (!empty($elements['#attached_' . $kind]) && is_array($elements['#attached_' . $kind])) {
foreach ($elements['#attached_' . $kind] as $data => $options) {
// If the value is not an array, it's a filename and passed as first
// (and only) argument.
if (!is_array($options)) {
$data = $options;
$options = NULL;
}
// When drupal_add_js with 'type' => 'setting' is called, the first
// parameter ($data) is an array. Arrays can't be keys in PHP, so we
// have to get $data from the value array.
if (is_numeric($data)) {
$data = $options['data'];
unset($options['data']);
}
call_user_func('drupal_add_' . $kind, $data, $options);
}
}
}
// Get the children of the element, sorted by weight.
$children = element_children($elements, TRUE);
......@@ -3750,6 +3772,9 @@ function drupal_common_theme() {
'text_format_wrapper' => array(
'arguments' => array('element' => NULL),
),
'vertical_tabs' => array(
'arguments' => array('element' => NULL),
),
);
}
......
......@@ -227,6 +227,7 @@ function form_state_defaults() {
'method' => 'post',
'rerender' => TRUE,
'programmed' => FALSE,
'groups' => array(),
);
}
......@@ -286,6 +287,10 @@ function drupal_rebuild_form($form_id, &$form_state, $form_build_id = NULL) {
// then process the form for rendering.
$form_state['input'] = array();
// Also clear out all group associations as these might be different
// when rerendering the form.
$form_state['groups'] = array();
// Do not call drupal_process_form(), since it would prevent the rebuilt form
// to submit.
$form = form_builder($form_id, $form, $form_state);
......@@ -939,7 +944,8 @@ function form_error(&$element, $message = '') {
/**
* Walk through the structured form array, adding any required
* properties to each element and mapping the incoming $_POST
* data to the proper elements.
* data to the proper elements. Also, execute any #process handlers
* attached to a specific element.
*
* @param $form_id
* A unique string identifying the form for validation, submission,
......@@ -972,9 +978,22 @@ function form_builder($form_id, $form, &$form_state) {
}
}
if (!isset($form['#id'])) {
$form['#id'] = form_clean_id('edit-' . implode('-', $form['#parents']));
}
if (isset($form['#input']) && $form['#input']) {
_form_builder_handle_input_element($form_id, $form, $form_state, $complete_form);
}
// Allow for elements to expand to multiple elements, e.g., radios,
// checkboxes and files.
if (isset($form['#process']) && !$form['#processed']) {
foreach ($form['#process'] as $process) {
if (drupal_function_exists($process)) {
$form = $process($form, $form_state, $complete_form);
}
}
$form['#processed'] = TRUE;
}
$form['#defaults_loaded'] = TRUE;
// We start off assuming all form elements are in the correct order.
......@@ -1062,8 +1081,7 @@ function form_builder($form_id, $form, &$form_state) {
/**
* Populate the #value and #name properties of input elements so they
* can be processed and rendered. Also, execute any #process handlers
* attached to a specific element.
* can be processed and rendered.
*/
function _form_builder_handle_input_element($form_id, &$form, &$form_state, $complete_form) {
if (!isset($form['#name'])) {
......@@ -1080,9 +1098,6 @@ function _form_builder_handle_input_element($form_id, &$form, &$form_state, $com
}
array_unshift($form['#parents'], $name);
}
if (!isset($form['#id'])) {
$form['#id'] = form_clean_id('edit-' . implode('-', $form['#parents']));
}
if (!empty($form['#disabled'])) {
$form['#attributes']['disabled'] = 'disabled';
......@@ -1151,16 +1166,6 @@ function _form_builder_handle_input_element($form_id, &$form, &$form_state, $com
}
}
}
// Allow for elements to expand to multiple elements, e.g., radios,
// checkboxes and files.
if (isset($form['#process']) && !$form['#processed']) {
foreach ($form['#process'] as $process) {
if (drupal_function_exists($process)) {
$form = $process($form, isset($edit) ? $edit : NULL, $form_state, $complete_form);
}
}
$form['#processed'] = TRUE;
}
form_set_value($form, $form['#value'], $form_state);
}
......@@ -1597,6 +1602,7 @@ function theme_fieldset($element) {
$element['#attributes']['class'] .= ' collapsed';
}
}
$element['#attributes']['id'] = $element['#id'];
return '<fieldset' . drupal_attributes($element['#attributes']) . '>' . ($element['#title'] ? '<legend>' . $element['#title'] . '</legend>' : '') . (isset($element['#description']) && $element['#description'] ? '<div class="description">' . $element['#description'] . '</div>' : '') . (!empty($element['#children']) ? $element['#children'] : '') . (isset($element['#value']) ? $element['#value'] : '') . "</fieldset>\n";
}
......@@ -2143,7 +2149,6 @@ function form_process_checkboxes($element) {
* An associative array containing the properties and children of the
* tableselect element.
* Properties used: header, options, empty, js_select.
*
* @return
* A themed HTML string representing the table.
*
......@@ -2186,7 +2191,6 @@ function theme_tableselect($element) {
* @param $element
* An associative array containing the properties and children of the
* tableselect element.
*
* @return
* The processed element.
*/
......@@ -2247,6 +2251,142 @@ function form_process_tableselect($element) {
return $element;
}
/**
* Adds fieldsets to the specified group or adds group members to this
* fieldset.
*
* @param $element
* An associative array containing the properties and children of the
* fieldset.
* @param $form_state
* The $form_state array for the form this fieldset belongs to.
* @return
* The processed element.
*/
function form_process_fieldset(&$element, &$form_state) {
$parents = implode('][', $element['#parents']);
// Add this fieldset to a group if one is set and if it's not being
// added to itself.
if (isset($element['#group']) && $element['#group'] != $parents) {
if (isset($form_state['groups'][$element['#group']]) && !empty($form_state['groups'][$element['#group']]['#group_exists'])) {
// Trick drupal_render() into believing this has already been output.
// The group widget will rerender this later. This only happens when the
// group is actually defined ('#group_exists' is TRUE). This prevents
// fieldsets from disappearing when the group they are associated to
// does not exist.
// If the group does not exist yet, the element's #printed value is left
// as is. As soon as the group is processed (fieldsets are also groups;
// see below), this element's #printed value is set to TRUE to prevent
// rendering in the original context.
$element['#printed'] = TRUE;
}
// Store a reference to this fieldset for the vertical tabs processing
// function.
$form_state['groups'][$element['#group']][] = &$element;
}
// Each fieldset can be a group itself and gets a reference to all
// elements in its group.
$form_state['groups'][$parents]['#group_exists'] = TRUE;
// There might already be elements associated with this group. Since the
// group did not exist yet at the time they were added to this group, they
// couldn't set #printed to TRUE (see above). We now know that this group
// does in fact exist and set #printed to TRUE to prevent rendering in the
// original context.
foreach (element_children($form_state['groups'][$parents]) as $key) {
$form_state['groups'][$parents][$key]['#printed'] = TRUE;
}
$element['#group_members'] = &$form_state['groups'][$parents];
// Contains form element summary functionalities.
drupal_add_js('misc/form.js', array('weight' => JS_LIBRARY + 1));
return $element;
}
/**
* Adds members of this group as actual elements for rendering.
*
* @param $element
* An associative array containing the properties and children of the
* fieldset.
* @return
* The modified element with all group members.
*/
function form_pre_render_fieldset($element) {
if (!empty($element['#group_members'])) {
// Add the group members to this fieldset for rendering purposes only.
foreach (element_children($element['#group_members']) as $key) {
// This was set in form_process_fieldset so that fieldsets which are
// added to groups are not rendered at their original location.
// drupal_render_children() will set this back to TRUE.
unset($element['#group_members'][$key]['#printed']);
$element[] = &$element['#group_members'][$key];
}
// Resort the element's children after the group members have been added.
$element['#sorted'] = FALSE;
}
return $element;
}
/**
* Creates a group formatted as vertical tabs.
*
* @param $element
* An associative array containing the properties and children of the
* fieldset.
* @param $form_state
* The $form_state array for the form this vertical tab widget belongs to.
* @return
* The processed element.
*/
function form_process_vertical_tabs($element, &$form_state) {
// To save us from modifying the existing element and changing its #type,
// a new form element is created as a child. The default #process hooks
// are called automatically by the form renderer and we don't have to do
// that manually.
$element['group'] = array(
'#type' => 'fieldset',
'#theme_wrapper' => '',
'#parents' => $element['#parents'],
);
// The JavaScript stores the currently selected tab in this hidden
// field so that the active tab can be restored the next time the
// form is rendered, e.g. on preview pages or when form validation
// fails.
$name = implode('__', $element['#parents']);
if (isset($form_state['values'][$name . '__active_tab'])) {
$element['#default_tab'] = $form_state['values'][$name . '__active_tab'];
}
$element[$name . '__active_tab'] = array(
'#type' => 'hidden',
'#default_value' => $element['#default_tab'],
'#attributes' => array('class' => 'vertical-tabs-active-tab'),
);
return $element;
}
/**
* Makes the element's children fieldsets be vertical tabs.
*
* @param $element
* An associative array containing the properties and children of the
* fieldset.
*/
function theme_vertical_tabs(&$element) {
// Add required JavaScript and Stylesheet.
drupal_add_js('misc/vertical-tabs.js', array('weight' => JS_DEFAULT - 1));
drupal_add_css('misc/vertical-tabs.css');
return '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
}
/**
* Theme a form submit button.
*
......
......@@ -60,6 +60,14 @@ Drupal.behaviors.collapse = {
fieldset.removeClass('collapsed');
}
var summary = $('<span class="summary"></span>');
fieldset.
bind('summaryUpdated', function() {
var text = $.trim(fieldset.getSummary());
summary.html(text ? ' (' + text + ')' : '');
})
.trigger('summaryUpdated');
// Turn the legend into a clickable link and wrap the contents of the fieldset
// in a div for easier animation
var text = this.innerHTML;
......@@ -72,6 +80,7 @@ Drupal.behaviors.collapse = {
}
return false;
}))
.append(summary)
.after($('<div class="fieldset-wrapper"></div>')
.append(fieldset.children(':not(legend):not(.action)')))
.addClass('collapse-processed');
......
// $Id$
(function($) {
/**
* Retrieves the summary for the first element.
*/
$.fn.getSummary = function() {
var callback = this.data('summaryCallback');
return (this[0] && callback) ? $.trim(callback(this[0])) : '';
};
/**
* Sets the summary for all matched elements.
*
* @param callback
* Either a function that will be called each time the summary is
* retrieved or a string (which is returned each time).
*/
$.fn.setSummary = function(callback) {
var that = this;
// To facilitate things, the callback should always be a function. If it's
// not, we wrap it into an anonymous function which just returns the value.
if (typeof callback != 'function') {
var val = callback;
callback = function() { return val; };
}
return this
.data('summaryCallback', callback)
// To prevent duplicate events, the handlers are first removed and then
// (re-)added.
.unbind('formUpdated.summary')
.bind('formUpdated.summary', function() {
that.trigger('summaryUpdated');
})
// The actual summaryUpdated handler doesn't fire when the callback is
// changed, so we have to do this manually.
.trigger('summaryUpdated');
};
/**
* Sends a 'formUpdated' event each time a form element is modified.
*/
Drupal.behaviors.formUpdated = {
attach: function(context) {
// These events are namespaced so that we can remove them later.
var events = 'change.formUpdated click.formUpdated blur.formUpdated keyup.formUpdated';
$(context)
// Since context could be an input element itself, it's added back to
// the jQuery object and filtered again.
.find(':input').andSelf().filter(':input')
// To prevent duplicate events, the handlers are first removed and then
// (re-)added.
.unbind(events).bind(events, function() {
$(this).trigger('formUpdated');
});
}
};
Drupal.behaviors.multiselectSelector = {
attach: function(context, settings) {
// Automatically selects the right radio button in a multiselect control.
......
/* $Id */
.vertical-tabs {
margin-left: 0;
margin-right: 15em;
}
.vertical-tabs-list {
margin-right: -15em;
right: 0;
float: right;
}
.vertical-tabs-list li.selected {
border-left-width: 0;
border-right-width: 1px;
}
/* $Id */
.vertical-tabs {
margin: 1em 0 1em 15em;
border: 1px solid #ccc;
}
.vertical-tabs-list {
width: 15em;
list-style: none;
list-style-image: none; /* IE6 */
border-top: 1px solid #ccc;
padding: 0;
position: relative; /* IE6 */
margin: -1px -100% -1px 0;
left: -15em;
float: left;
}
.vertical-tabs .vertical-tabs-panes fieldset.vertical-tabs-pane {
margin: 0 !important;
padding: 0 1em;
border: 0;
}
.vertical-tabs .vertical-tabs-panes fieldset.vertical-tabs-pane legend {
display: none;
}
/* Layout of each tab */
.vertical-tabs-list li {
background: #eee;
border: 1px solid #ccc;
border-top: 0;
padding: 0;
margin: 0;
height: 1%;
}
.vertical-tabs-list li a {
display: block;
text-decoration: none;
padding: 0.5em 0.6em;
line-height: 1.3em;
height: 1%;
}
.vertical-tabs-list li a:focus {
position: relative;
z-index: 5;
}
.vertical-tabs-list li a:hover {
text-decoration: none;
}
.vertical-tabs-list li.selected {
background: #fff;
border-right-width: 0;
position: relative;
}
.vertical-tabs-list li.selected a:focus {
outline: 0;
}
.vertical-tabs-list li.selected .title {
font-weight: bold;
color: #000;
}
.vertical-tabs-list .summary {
display: block;
}
.vertical-tabs ul.vertical-tabs-list .summary {
line-height: normal;
margin-bottom: 0;
}
// $Id$
(function($) {
/**
* This script transforms a set of fieldsets into a stack of vertical
* tabs. Another tab pane can be selected by clicking on the respective
* tab.
*
* Each tab may have a summary which can be updated by another
* script. For that to work, each fieldset has an associated
* 'verticalTabCallback' (with jQuery.data() attached to the fieldset),
* which is called every time the user performs an update to a form
* element inside the tab pane.
*/
Drupal.behaviors.verticalTabs = {
attach: function(context) {
$('.vertical-tabs-panes:not(.vertical-tabs-processed)', context).each(function() {
var focusID = $(':hidden.vertical-tabs-active-tab', this).val();
var focus;
// Create the tab column.
var list = $('<ul class="vertical-tabs-list"></ul>');
$(this).wrap('<div class="vertical-tabs clearfix"></div>').before(list);
// Transform each fieldset into a tab.
$('> fieldset', this).each(function() {
var tab = new Drupal.verticalTab({ title: $('> legend', this).text(), fieldset: $(this) });
list.append(tab.item);
$(this)
.removeClass('collapsible collapsed')
.addClass('vertical-tabs-pane')
.data('verticalTab', tab);
if (this.id == focusID) {
focus = $(this);
}
});
$('> li:first', list).addClass('first');
$('> li:last', list).addClass('last');
if (!focus) {
focus = $('> .vertical-tabs-pane:first', this);
}
focus.data('verticalTab').focus();
}).addClass('vertical-tabs-processed');
}
};
/**
* The vertical tab object represents a single tab within a tab group.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* - fieldset: The jQuery object of the fieldset that is the tab pane.
*/
Drupal.verticalTab = function(settings) {
var that = this;
$.extend(this, settings, Drupal.theme('verticalTab', settings));
this.link.click(function() {
that.focus();
return false;
});
this.fieldset
.bind('summaryUpdated', function() {
that.updateSummary();
})
.trigger('summaryUpdated');
};
Drupal.verticalTab.prototype = {
// Displays the tab's content pane.
focus: function() {
this.fieldset
.siblings('fieldset.vertical-tabs-pane')
.each(function() {
var tab = $(this).data('verticalTab');
tab.fieldset.hide();
tab.item.removeClass('selected');
})
.end()
.show()
.siblings(':hidden.vertical-tabs-active-tab')
.val(this.fieldset.attr('id'));
this.item.addClass('selected');
},
// Updates the tab's summary.
updateSummary: function() {
this.summary.html(this.fieldset.getSummary());
}
};
/**
* Theme function for a vertical tab.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* @return
* This function has to return an object with at least these keys:
* - item: The root tab jQuery element
* - link: The anchor tag that acts as the clickable area of the tab
* (jQuery version)
* - summary: The jQuery element that contains the tab summary
*/
Drupal.theme.prototype.verticalTab = function(settings) {
var tab = {};
tab.item = $('<li class="vertical-tab-button"></li>')
.append(tab.link = $('<a href="#"></a>')
.append(tab.title = $('<span class="title"></span>').text(settings.title))
.append(tab.summary = $('<span class="summary"></span>')
)
);
return tab;
};
})(jQuery);
......@@ -35,6 +35,9 @@
margin-top: 0;
margin-bottom: 0;
}
html.js #edit-book-pick-book {
display: none;
}
#edit-book-bid-wrapper .description {
clear: both;
}
......
// $Id$
(function($) {
Drupal.behaviors.bookFieldsetSummaries = {
attach: function(context) {
$('fieldset#edit-book', context).setSummary(function(context) {
var val = $('#edit-book-bid').val();
if (val === '0') {
return Drupal.t('Not in book');
}
else if (val === 'new') {
return Drupal.t('New book');
}
else {
return Drupal.checkPlain($('#edit-book-bid :selected').text());
}
});
}
};
})(jQuery);
......@@ -415,7 +415,6 @@ function _book_parent_select($book_link) {
function _book_add_form_elements(&$form, $node) {
// Need this for AJAX.
$form['#cache'] = TRUE;
drupal_add_js("if (Drupal.jsEnabled) { jQuery(function() { jQuery('#edit-book-pick-book').css('display', 'none'); }); }", 'inline');
$form['book'] = array(
'#type' => 'fieldset',
......@@ -423,6 +422,8 @@ function _book_add_form_elements(&$form, $node) {
'#weight' => 10,
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#group' => 'additional_settings',
'#attached_js' => array(drupal_get_path('module', 'book') . '/book.js'),
'#tree' => TRUE,
'#attributes' => array('class' => 'book-outline-form'),
);
......
// $Id$
(function($) {
Drupal.behaviors.commentFieldsetSummaries = {
attach: function(context) {
$('fieldset#edit-comment-settings', context).setSummary(function(context) {