Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.777 diff -u -r1.777 system.module --- modules/system/system.module 26 Aug 2009 10:53:45 -0000 1.777 +++ modules/system/system.module 26 Aug 2009 17:15:11 -0000 @@ -481,6 +481,11 @@ '#process' => array('form_process_vertical_tabs'), ); + $type['wrapper'] = array( + '#theme_wrappers' => array('wrapper'), + '#process' => array('form_process_wrapper'), + ); + $type['token'] = array( '#input' => TRUE, '#theme' => array('hidden'), Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.197 diff -u -r1.197 system.admin.inc --- modules/system/system.admin.inc 26 Aug 2009 10:53:45 -0000 1.197 +++ modules/system/system.admin.inc 26 Aug 2009 17:15:11 -0000 @@ -480,6 +480,9 @@ '#title' => t('Logo image settings'), '#description' => t('If toggled on, the following logo will be displayed.'), '#attributes' => array('class' => array('theme-settings-bottom')), + '#dependencies' => array( + 'invisible' => array('input[name="toggle_logo"]' => array('checked' => FALSE)), + ), ); $form['logo']['default_logo'] = array( '#type' => 'checkbox', @@ -492,13 +495,20 @@ '#type' => 'textfield', '#title' => t('Path to custom logo'), '#default_value' => $settings['logo_path'], - '#description' => t('The path to the file you would like to use as your logo file instead of the default logo.')); + '#description' => t('The path to the file you would like to use as your logo file instead of the default logo.'), + '#dependencies' => array( + 'invisible' => array('input[name="default_logo"]' => array('checked' => TRUE)), + ), + ); $form['logo']['logo_upload'] = array( '#type' => 'file', '#title' => t('Upload logo image'), '#maxlength' => 40, - '#description' => t("If you don't have direct file access to the server, use this field to upload your logo.") + '#description' => t("If you don't have direct file access to the server, use this field to upload your logo."), + '#dependencies' => array( + 'invisible' => array('input[name="default_logo"]' => array('checked' => TRUE)), + ), ); } @@ -507,6 +517,9 @@ '#type' => 'fieldset', '#title' => t('Shortcut icon settings'), '#description' => t("Your shortcut icon, or 'favicon', is displayed in the address bar and bookmarks of most browsers."), + '#dependencies' => array( + 'invisible' => array('input[name="toggle_favicon"]' => array('checked' => FALSE)), + ), ); $form['favicon']['default_favicon'] = array( '#type' => 'checkbox', @@ -518,13 +531,19 @@ '#type' => 'textfield', '#title' => t('Path to custom icon'), '#default_value' => $settings['favicon_path'], - '#description' => t('The path to the image file you would like to use as your custom shortcut icon.') + '#description' => t('The path to the image file you would like to use as your custom shortcut icon.'), + '#dependencies' => array( + 'invisible' => array('input[name="default_favicon"]' => array('checked' => TRUE)), + ), ); $form['favicon']['favicon_upload'] = array( '#type' => 'file', '#title' => t('Upload icon image'), - '#description' => t("If you don't have direct file access to the server, use this field to upload your shortcut icon.") + '#description' => t("If you don't have direct file access to the server, use this field to upload your shortcut icon."), + '#dependencies' => array( + 'invisible' => array('input[name="default_favicon"]' => array('checked' => TRUE)), + ), ); } @@ -1670,17 +1689,18 @@ '#default_value' => $configurable_timezones, ); - $js_hide = !$configurable_timezones ? ' class="js-hide"' : ''; $form['timezone']['configurable_timezones_wrapper'] = array( - '#prefix' => '
', - '#suffix' => '
', + '#type' => 'wrapper', + '#dependencies' => array( + 'invisible' => array('input[name="configurable_timezones"]' => array('checked' => FALSE)) + ), ); $form['timezone']['configurable_timezones_wrapper']['empty_timezone_message'] = array( '#type' => 'checkbox', '#title' => t('Remind users at login if their time zone is not set.'), '#default_value' => variable_get('empty_timezone_message', 0), - '#description' => t('Only applied if users may set their own time zone.') + '#description' => t('Only applied if users may set their own time zone.'), ); $form['timezone']['configurable_timezones_wrapper']['user_default_timezone'] = array( @@ -1692,7 +1712,7 @@ DRUPAL_USER_TIMEZONE_EMPTY => t('Empty time zone.'), DRUPAL_USER_TIMEZONE_SELECT => t('Users may set their own time zone at registration.'), ), - '#description' => t('Only applied if users may set their own time zone.') + '#description' => t('Only applied if users may set their own time zone.'), ); $form['date_formats'] = array( Index: modules/system/system.js =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.js,v retrieving revision 1.33 diff -u -r1.33 system.js --- modules/system/system.js 24 Aug 2009 22:03:01 -0000 1.33 +++ modules/system/system.js 26 Aug 2009 17:15:11 -0000 @@ -123,19 +123,6 @@ }; /** - * Show/hide settings for user configurable time zones depending on whether - * users are able to set their own time zones or not. - */ -Drupal.behaviors.userTimeZones = { - attach: function (context, settings) { - $('#empty-timezone-message-wrapper .description').hide(); - $('#edit-configurable-timezones', context).change(function () { - $('#empty-timezone-message-wrapper').toggle(); - }); - }, -}; - -/** * Show the powered by Drupal image preview */ Drupal.behaviors.poweredByPreview = { Index: modules/node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.78 diff -u -r1.78 node.pages.inc --- modules/node/node.pages.inc 25 Aug 2009 10:27:14 -0000 1.78 +++ modules/node/node.pages.inc 26 Aug 2009 17:15:11 -0000 @@ -180,6 +180,9 @@ '#type' => 'checkbox', '#title' => t('Create new revision'), '#default_value' => $node->revision, + '#dependencies' => array( + 'checked' => array('textarea[name="log"]' => array('empty' => FALSE)) + ), ); $form['revision_information']['log'] = array( '#type' => 'textarea', Index: modules/user/user.js =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.js,v retrieving revision 1.16 diff -u -r1.16 user.js --- modules/user/user.js 21 Aug 2009 14:27:47 -0000 1.16 +++ modules/user/user.js 26 Aug 2009 17:15:11 -0000 @@ -160,16 +160,4 @@ return { strength: strength, message: msg }; }; -/** - * Show all of the picture-related form elements at admin/config/people/accounts - * depending on whether user pictures are enabled or not. - */ -Drupal.behaviors.userSettings = { - attach: function (context, settings) { - $('#edit-user-pictures', context).change(function () { - $('div.user-admin-picture-settings', context).toggle(); - }); - } -}; - })(jQuery); Index: modules/user/user.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.admin.inc,v retrieving revision 1.75 diff -u -r1.75 user.admin.inc --- modules/user/user.admin.inc 24 Aug 2009 00:14:23 -0000 1.75 +++ modules/user/user.admin.inc 26 Aug 2009 17:15:11 -0000 @@ -337,15 +337,11 @@ '#default_value' => $picture_support, ); drupal_add_js(drupal_get_path('module', 'user') . '/user.js'); - // If JS is enabled, and the checkbox defaults to off, hide all the settings - // on page load via CSS using the js-hide class so there's no flicker. - $css_class = 'user-admin-picture-settings'; - if (!$picture_support) { - $css_class .= ' js-hide'; - } $form['personalization']['pictures'] = array( - '#prefix' => '
', - '#suffix' => '
', + '#type' => 'wrapper', + '#dependencies' => array( + 'invisible' => array('input[name="user_pictures"]' => array('checked' => FALSE)), + ), ); $form['personalization']['pictures']['user_picture_path'] = array( '#type' => 'textfield', @@ -511,12 +507,18 @@ '#title' => t('Subject'), '#default_value' => _user_mail_text('status_activated_subject'), '#maxlength' => 180, + '#dependencies' => array( + 'invisible' => array('input[name="user_mail_status_activated_notify"]' => array('checked' => FALSE)), + ), ); $form['email_activated']['user_mail_status_activated_body'] = array( '#type' => 'textarea', '#title' => t('Body'), '#default_value' => _user_mail_text('status_activated_body'), '#rows' => 15, + '#dependencies' => array( + 'invisible' => array('input[name="user_mail_status_activated_notify"]' => array('checked' => FALSE)), + ), ); $form['email_blocked'] = array( @@ -537,12 +539,18 @@ '#title' => t('Subject'), '#default_value' => _user_mail_text('status_blocked_subject'), '#maxlength' => 180, + '#dependencies' => array( + 'invisible' => array('input[name="user_mail_status_blocked_notify"]' => array('checked' => FALSE)), + ), ); $form['email_blocked']['user_mail_status_blocked_body'] = array( '#type' => 'textarea', '#title' => t('Body'), '#default_value' => _user_mail_text('status_blocked_body'), '#rows' => 3, + '#dependencies' => array( + 'invisible' => array('input[name="user_mail_status_blocked_notify"]' => array('checked' => FALSE)), + ), ); $form['email_cancel_confirm'] = array( @@ -584,12 +592,18 @@ '#title' => t('Subject'), '#default_value' => _user_mail_text('status_canceled_subject'), '#maxlength' => 180, + '#dependencies' => array( + 'invisible' => array('input[name="user_mail_status_canceled_notify"]' => array('checked' => FALSE)), + ), ); $form['email_canceled']['user_mail_status_canceled_body'] = array( '#type' => 'textarea', '#title' => t('Body'), '#default_value' => _user_mail_text('status_canceled_body'), '#rows' => 3, + '#dependencies' => array( + 'invisible' => array('input[name="user_mail_status_canceled_notify"]' => array('checked' => FALSE)), + ), ); return system_settings_form($form, FALSE); Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.977 diff -u -r1.977 common.inc --- includes/common.inc 26 Aug 2009 15:00:17 -0000 1.977 +++ includes/common.inc 26 Aug 2009 17:15:11 -0000 @@ -4033,6 +4033,12 @@ isset($elements['#attached_css']) ? $elements['#attached_css'] : array() ); + // Add the dependency information for this form element. + if (!empty($elements['#dependencies'])) { + drupal_add_js('misc/dependencies.js', array('weight' => JS_LIBRARY + 1)); + drupal_add_js(array('dependencies' => array('#' . $elements['#id'] => $elements['#dependencies'])), 'setting'); + } + $prefix = isset($elements['#prefix']) ? $elements['#prefix'] : ''; $suffix = isset($elements['#suffix']) ? $elements['#suffix'] : ''; @@ -4425,6 +4431,9 @@ 'vertical_tabs' => array( 'arguments' => array('element' => NULL), ), + 'wrapper' => array( + 'arguments' => array('element' => NULL), + ), ); } Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.366 diff -u -r1.366 form.inc --- includes/form.inc 25 Aug 2009 21:16:31 -0000 1.366 +++ includes/form.inc 26 Aug 2009 17:15:11 -0000 @@ -2306,6 +2306,38 @@ } /** + * Processes a wrapper element. + * + * @param $element + * An associative array containing the properties and children of the + * wrapper. + * @param $form_state + * The $form_state array for the form this element belongs to. + * @return + * The processed element. + */ +function form_process_wrapper($element, &$form_state) { + $element['#id'] = form_clean_id(implode('-', $element['#parents']) . '-wrapper'); + return $element; +} + +/** + * Adds a wrapper for grouping items + * + * @param $element + * An associative array containing the properties and children of the + * group. + * Properties used: #children. + * @return + * A themed HTML string representing the form element. + * + * @ingroup themeable + */ +function theme_wrapper($element) { + return '
' . $element['#children'] . '
'; +} + +/** * Theme a submit button form element. * * @param $element Index: modules/menu/menu.module =================================================================== RCS file: /cvs/drupal/drupal/modules/menu/menu.module,v retrieving revision 1.200 diff -u -r1.200 menu.module --- modules/menu/menu.module 24 Aug 2009 00:14:21 -0000 1.200 +++ modules/menu/menu.module 26 Aug 2009 17:15:11 -0000 @@ -435,11 +435,21 @@ } $form['menu']['#item'] = $item; + $form['menu']['has_menu'] = array( + '#type' => 'checkbox', + '#title' => t('Add menu item'), + '#description' => t('Check if you want to assign a menu item to this content.'), + '#default_value' => !empty($item['link_title']), + ); + $form['menu']['link_title'] = array('#type' => 'textfield', '#title' => t('Menu link title'), '#default_value' => $item['link_title'], '#description' => t('The link text corresponding to this item that should appear in the menu. Leave blank if you do not wish to add this post to the menu.'), '#required' => FALSE, + '#dependencies' => array( + 'enabled' => array('input[name="menu[has_menu]"]' => array('checked' => TRUE)) + ), ); // Generate a list of possible parents (not including this item or descendants). $options = menu_parent_options(menu_get_menus(), $item); @@ -454,6 +464,9 @@ '#options' => $options, '#description' => t('The maximum depth for an item and all its children is fixed at !maxdepth. Some menu items may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)), '#attributes' => array('class' => array('menu-title-select')), + '#dependencies' => array( + 'enabled' => array('input[name="menu[has_menu]"]' => array('checked' => TRUE)) + ), ); $form['#submit'][] = 'menu_node_form_submit'; @@ -463,6 +476,9 @@ '#delta' => 50, '#default_value' => $item['weight'], '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'), + '#dependencies' => array( + 'enabled' => array('input[name="menu[has_menu]"]' => array('checked' => TRUE)) + ), ); } } Index: misc/dependencies.js =================================================================== RCS file: misc/dependencies.js diff -N misc/dependencies.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ misc/dependencies.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,286 @@ +// $Id$ +(function ($) { + +var dependencies = Drupal.dependencies = {}; +dependencies.debug = 'console' in window && false; + +/** + * Attaches the dependencies. + */ +Drupal.behaviors.dependencies = { + attach: function (context, settings) { + dependencies.initializing = true; + dependencies.postponed = []; + + $.each(settings.dependencies, function (selector) { + $.each(this, function (state, dependees) { + new dependencies.Dependant({ + element: $(selector), + state: dependencies.State.sanitize(state), + dependees: dependees + }); + }); + }); + + while (dependencies.postponed.length) { + (dependencies.postponed.shift())(); + } + dependencies.initializing = false; + } +}; + +dependencies.Dependant = function (args) { + $.extend(this, { values: {}, oldValue: undefined }, args); + + for (var selector in this.dependees) { + this.initializeDependee(selector, this.dependees[selector]); + } +}; + +dependencies.Dependant.comparisons = { + 'RegExp': function (reference, value) { + return reference.test(value); + }, + 'Function': function (reference, value) { + return reference(value); + } +}; + +dependencies.Dependant.prototype = { + initializeDependee: function (selector, states) { + var self = this; + self.values[selector] = {}; + + $.each(states, function (state, value) { + state = dependencies.State.sanitize(state); + self.values[selector][state.pristine] = undefined; + + // bind to the trigger and update internal represenation of state + $(selector).bind('state:' + state, function (e) { + var complies = self.compare(value, e.value); + self.update(selector, state, complies); + }); + + new dependencies.Trigger({ selector: selector, state: state }); + }); + }, + + compare: function (reference, value) { + if (reference.constructor.name in dependencies.Dependant.comparisons) { + return dependencies.Dependant.comparisons[reference.constructor.name](reference, value); + } + else { + return reference === value; + } + }, + + callback: function (value) { + value = invert(value, this.state.invert); + this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true }); + }, + + update: function (selector, state, value) { + if (value !== this.values[selector][state.pristine]) { + this.values[selector][state.pristine] = value; + this.reevaluate(); + } + }, + + reevaluate: function () { + var value = undefined; + + for (var selector in this.values) { + for (var state in this.values[selector]) { + state = dependencies.State.sanitize(state); + var complies = this.values[selector][state.pristine]; + value = ternary(value, invert(complies, state.invert)); + } + } + + if (value !== this.oldValue) { + this.oldValue = value; + this.callback(value); + } + } +}; + + + +dependencies.Trigger = function (args) { + $.extend(this, args); + + if (this.state in dependencies.Trigger.states) { + this.element = $(this.selector); + + // Only call the trigger initializer when it wasn't yet attached to this + // element. Otherwise we'd end up with duplicate events. + if (!this.element.data('trigger:' + this.state)) { + this.initialize(); + } + } +}; + +dependencies.Trigger.prototype = { + initialize: function () { + var self = this; + var trigger = dependencies.Trigger.states[this.state]; + + if (typeof trigger == 'function') { + // We have a custom trigger initialization function. + trigger.call(window, this.element); + } + else { + $.each(trigger, function (event, valueFn) { + self.defaultTrigger(event, valueFn); + }); + } + + // Mark this trigger as initialized for this element. + this.element.data('trigger:' + this.state, true); + }, + + defaultTrigger: function (event, valueFn) { + var self = this; + var oldValue = valueFn.call(this.element); + + // Attach the event callback. + this.element[event](function (e) { + var value = valueFn.call(self.element); + // Only trigger the event if the value has actually changed. + if (oldValue !== value) { + if (dependencies.debug) console.log('firing %o with value %o for %o', 'state:' + self.state, value, this); + self.element.trigger({ type: 'state:' + self.state, value: value, oldValue: oldValue }); + oldValue = value; + } + }); + + dependencies.postponed.push(function () { + // Trigger the event once for initialization purposes. + self.element.trigger({ type: 'state:' + self.state, value: oldValue, oldValue: undefined }); + }); + } +}; + + +dependencies.Trigger.states = { + empty: { + 'keyup': function (e) { + return this.val() == ''; + } + }, + + checked: { + 'change': function (e) { + return this.attr('checked'); + } + }, + + value: { + 'keyup': function (e) { + return this.val(); + } + } +}; + + + +dependencies.State = function(state) { + // We may need the original unresolved name later. + this.pristine = this.name = state; + + // Normalize the state name. + while(true) { + // Iteratively remove exclamation marks and invert the value. + while (this.name.charAt(0) == '!') { + this.name = this.name.substring(1); + this.invert = !this.invert; + } + + // Replace the state with its normalized name. + if (this.name in dependencies.State.aliases) + this.name = dependencies.State.aliases[this.name]; + else + break; + } +}; + +// Make sure the state is a state object. +dependencies.State.sanitize = function (state) { + if (state instanceof dependencies.State) { + return state; + } + else { + return new dependencies.State(state); + } +}; + +dependencies.State.aliases = { + 'enabled': '!disabled', + 'invisible': '!visible', + 'invalid': '!valid', + 'untouched': '!touched', + 'optional': '!required', + 'filled': '!empty', + 'unchecked': '!checked', + 'irrelevant': '!relevant', + 'expanded': '!collapsed', + 'readwrite': '!readonly', + 'collapsed': '!expanded' +}; + +// dependencies.State: prototype +dependencies.State.prototype = { + invert: false, + + toString: function() { + return this.name; + } +}; + + + +// Global state change handlers +{ + $(document).bind('state:disabled', function(e) { + if (e.trigger) { + $(e.target) + .attr('disabled', e.value) + .filter('.form-element') + .closest('.form-item, .form-wrapper, fieldset')[e.value ? 'addClass' : 'removeClass']('form-disabled'); + + // Note: WebKit nightlies don't reflect that change correctly. + // See https://bugs.webkit.org/show_bug.cgi?id=23789 + } + }); + + $(document).bind('state:required', function(e) { + if (e.trigger) { + $(e.target).closest('.form-item, .form-wrapper, fieldset')[e.value ? 'addClass' : 'removeClass']('form-required'); + } + }); + + $(document).bind('state:visible', function(e) { + if (e.trigger) { + $(e.target).closest('.form-item, .form-wrapper, fieldset')[e.value ? 'show' : 'hide'](); + } + }); + + $(document).bind('state:checked', function(e) { + if (e.trigger) { + $(e.target).attr('checked', e.value); + } + }); +} + + +// Bitwise AND with a third undefined state. +function ternary(a, b) { + return a === undefined ? b : (b === undefined ? a : a && b); +}; + +// Inverts a (if it's not undefined) when invert is true. +function invert(a, invert) { + return (invert && a !== undefined) ? !a : a; +}; + +})(jQuery);