commit 2f4253e2d6fa190fc4c88b33046d5e39673e2c0c
Author: Konstantin Käfer <github@kkaefer.com>
Date:   Sun Aug 23 19:31:14 2009 +0200

    initial working version of the dependency system with two sample dependencies on the node edit form: revision log and menu item

diff --git a/includes/common.inc b/includes/common.inc
index 5da70b9..81ae6ee 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -4019,6 +4019,12 @@ function drupal_render(&$elements) {
     }
   }
 
+  // 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'] : '';
 
diff --git a/misc/dependencies.js b/misc/dependencies.js
new file mode 100644
index 0000000..e076105
--- /dev/null
+++ b/misc/dependencies.js
@@ -0,0 +1,286 @@
+// $Id$
+(function ($) {
+
+var _ = Drupal.dependencies = {};
+_.debug = 'console' in window && false;
+
+/**
+ * Attaches the dependencies.
+ */
+Drupal.behaviors.dependencies = {
+  attach: function (context, settings) {
+    _.initializing = true;
+    _.postponed = [];
+
+    $.each(settings.dependencies, function (selector) {
+      $.each(this, function (state, dependees) {
+        new _.Dependant({
+          element: $(selector),
+          state: _.State.sanitize(state),
+          dependees: dependees
+        });
+      });
+    });
+
+    while (_.postponed.length) {
+      (_.postponed.shift())();
+    }
+    _.initializing = false;
+  }
+};
+
+_.Dependant = function (args) {
+  $.extend(this, { values: {}, oldValue: undefined }, args);
+
+  for (var selector in this.dependees) {
+    this.initializeDependee(selector, this.dependees[selector]);
+  }
+};
+
+_.Dependant.comparisons = {
+  'RegExp': function (reference, value) {
+    return reference.test(value);
+  },
+  'Function': function (reference, value) {
+    return reference(value);
+  }
+};
+
+_.Dependant.prototype = {
+  initializeDependee: function (selector, states) {
+    var self = this;
+    self.values[selector] = {};
+
+    $.each(states, function (state, value) {
+      state = _.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 _.Trigger({ selector: selector, state: state });
+    });
+  },
+
+  compare: function (reference, value) {
+    if (reference.constructor.name in _.Dependant.comparisons) {
+      return _.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 = _.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);
+    }
+  }
+};
+
+
+
+_.Trigger = function (args) {
+  $.extend(this, args);
+
+  if (this.state in _.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();
+    }
+  }
+};
+
+_.Trigger.prototype = {
+  initialize: function () {
+    var self = this;
+    var trigger = _.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 (_.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;
+      }
+    });
+
+    _.postponed.push(function () {
+      // Trigger the event once for initialization purposes.
+      self.element.trigger({ type: 'state:' + self.state, value: oldValue, oldValue: undefined });
+    });
+  }
+};
+
+
+_.Trigger.states = {
+  empty: {
+    'keyup': function (e) {
+      return this.val() == '';
+    }
+  },
+
+  checked: {
+    'change': function (e) {
+      return this.attr('checked');
+    }
+  },
+
+  value: {
+    'keyup': function (e) {
+      return this.val();
+    }
+  }
+};
+
+
+
+_.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 _.State.aliases)
+      this.name = _.State.aliases[this.name];
+    else
+      break;
+  }
+};
+
+// Make sure the state is a state object.
+_.State.sanitize = function (state) {
+  if (state instanceof _.State) {
+    return state;
+  }
+  else {
+    return new _.State(state);
+  }
+};
+
+_.State.aliases = {
+  'enabled': '!disabled',
+  'invisible': '!visible',
+  'invalid': '!valid',
+  'untouched': '!touched',
+  'optional': '!required',
+  'filled': '!empty',
+  'unchecked': '!checked',
+  'irrelevant': '!relevant',
+  'expanded': '!collapsed',
+  'readwrite': '!readonly',
+  'collapsed': '!expanded'
+};
+
+// _.State: prototype
+_.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')[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')[e.value ? 'addClass' : 'removeClass']('form-required');
+    }
+  });
+
+  $(document).bind('state:visible', function(e) {
+    if (e.trigger) {
+      $(e.target).closest('.form-item')[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);
diff --git a/modules/menu/menu.module b/modules/menu/menu.module
index 46d181f..96e2df7 100644
--- a/modules/menu/menu.module
+++ b/modules/menu/menu.module
@@ -425,11 +425,21 @@ function menu_form_alter(&$form, $form_state, $form_id) {
     }
     $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('#edit-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);
@@ -444,6 +454,9 @@ function menu_form_alter(&$form, $form_state, $form_id) {
       '#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('#edit-menu-has-menu' => array('checked' => TRUE))
+      ),
     );
     $form['#submit'][] = 'menu_node_form_submit';
 
@@ -453,6 +466,9 @@ function menu_form_alter(&$form, $form_state, $form_id) {
       '#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('#edit-menu-has-menu' => array('checked' => TRUE))
+      ),
     );
   }
 }
diff --git a/modules/node/node.pages.inc b/modules/node/node.pages.inc
index 7b78c36..a118e9a 100644
--- a/modules/node/node.pages.inc
+++ b/modules/node/node.pages.inc
@@ -180,6 +180,9 @@ function node_form(&$form_state, $node) {
       '#type' => 'checkbox',
       '#title' => t('Create new revision'),
       '#default_value' => $node->revision,
+      '#dependencies' => array(
+        'checked' => array('#edit-log' => array('empty' => FALSE))
+      ),
     );
     $form['revision_information']['log'] = array(
       '#type' => 'textarea',
