| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 | (function ($) {/** * The base States namespace. * * Having the local states variable allows us to use the States namespace * without having to always declare "Drupal.states". */var states = Drupal.states = {  // An array of functions that should be postponed.  postponed: []};/** * Attaches the states. */Drupal.behaviors.states = {  attach: function (context, settings) {    var $context = $(context);    for (var selector in settings.states) {      for (var state in settings.states[selector]) {        new states.Dependent({          element: $context.find(selector),          state: states.State.sanitize(state),          constraints: settings.states[selector][state]        });      }    }    // Execute all postponed functions now.    while (states.postponed.length) {      (states.postponed.shift())();    }  }};/** * Object representing an element that depends on other elements. * * @param args *   Object with the following keys (all of which are required): *   - element: A jQuery object of the dependent element *   - state: A State object describing the state that is dependent *   - constraints: An object with dependency specifications. Lists all elements *     that this element depends on. It can be nested and can contain arbitrary *     AND and OR clauses. */states.Dependent = function (args) {  $.extend(this, { values: {}, oldValue: null }, args);  this.dependees = this.getDependees();  for (var selector in this.dependees) {    this.initializeDependee(selector, this.dependees[selector]);  }};/** * Comparison functions for comparing the value of an element with the * specification from the dependency settings. If the object type can't be * found in this list, the === operator is used by default. */states.Dependent.comparisons = {  'RegExp': function (reference, value) {    return reference.test(value);  },  'Function': function (reference, value) {    // The "reference" variable is a comparison function.    return reference(value);  },  'Number': function (reference, value) {    // If "reference" is a number and "value" is a string, then cast reference    // as a string before applying the strict comparison in compare(). Otherwise    // numeric keys in the form's #states array fail to match string values    // returned from jQuery's val().    return (typeof value === 'string') ? compare(reference.toString(), value) : compare(reference, value);  }};states.Dependent.prototype = {  /**   * Initializes one of the elements this dependent depends on.   *   * @param selector   *   The CSS selector describing the dependee.   * @param dependeeStates   *   The list of states that have to be monitored for tracking the   *   dependee's compliance status.   */  initializeDependee: function (selector, dependeeStates) {    var state;    // Cache for the states of this dependee.    this.values[selector] = {};    for (var i in dependeeStates) {      if (dependeeStates.hasOwnProperty(i)) {        state = dependeeStates[i];        // Make sure we're not initializing this selector/state combination twice.        if ($.inArray(state, dependeeStates) === -1) {          continue;        }        state = states.State.sanitize(state);        // Initialize the value of this state.        this.values[selector][state.name] = null;        // Monitor state changes of the specified state for this dependee.        $(selector).bind('state:' + state, $.proxy(function (e) {          this.update(selector, state, e.value);        }, this));        // Make sure the event we just bound ourselves to is actually fired.        new states.Trigger({ selector: selector, state: state });      }    }  },  /**   * Compares a value with a reference value.   *   * @param reference   *   The value used for reference.   * @param selector   *   CSS selector describing the dependee.   * @param state   *   A State object describing the dependee's updated state.   *   * @return   *   true or false.   */  compare: function (reference, selector, state) {    var value = this.values[selector][state.name];    if (reference.constructor.name in states.Dependent.comparisons) {      // Use a custom compare function for certain reference value types.      return states.Dependent.comparisons[reference.constructor.name](reference, value);    }    else {      // Do a plain comparison otherwise.      return compare(reference, value);    }  },  /**   * Update the value of a dependee's state.   *   * @param selector   *   CSS selector describing the dependee.   * @param state   *   A State object describing the dependee's updated state.   * @param value   *   The new value for the dependee's updated state.   */  update: function (selector, state, value) {    // Only act when the 'new' value is actually new.    if (value !== this.values[selector][state.name]) {      this.values[selector][state.name] = value;      this.reevaluate();    }  },  /**   * Triggers change events in case a state changed.   */  reevaluate: function () {    // Check whether any constraint for this dependent state is satisifed.    var value = this.verifyConstraints(this.constraints);    // Only invoke a state change event when the value actually changed.    if (value !== this.oldValue) {      // Store the new value so that we can compare later whether the value      // actually changed.      this.oldValue = value;      // Normalize the value to match the normalized state name.      value = invert(value, this.state.invert);      // By adding "trigger: true", we ensure that state changes don't go into      // infinite loops.      this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true });    }  },  /**   * Evaluates child constraints to determine if a constraint is satisfied.   *   * @param constraints   *   A constraint object or an array of constraints.   * @param selector   *   The selector for these constraints. If undefined, there isn't yet a   *   selector that these constraints apply to. In that case, the keys of the   *   object are interpreted as the selector if encountered.   *   * @return   *   true or false, depending on whether these constraints are satisfied.   */  verifyConstraints: function(constraints, selector) {    var result;    if ($.isArray(constraints)) {      // This constraint is an array (OR or XOR).      var hasXor = $.inArray('xor', constraints) === -1;      for (var i = 0, len = constraints.length; i < len; i++) {        if (constraints[i] != 'xor') {          var constraint = this.checkConstraints(constraints[i], selector, i);          // Return if this is OR and we have a satisfied constraint or if this          // is XOR and we have a second satisfied constraint.          if (constraint && (hasXor || result)) {            return hasXor;          }          result = result || constraint;        }      }    }    // Make sure we don't try to iterate over things other than objects. This    // shouldn't normally occur, but in case the condition definition is bogus,    // we don't want to end up with an infinite loop.    else if ($.isPlainObject(constraints)) {      // This constraint is an object (AND).      for (var n in constraints) {        if (constraints.hasOwnProperty(n)) {          result = ternary(result, this.checkConstraints(constraints[n], selector, n));          // False and anything else will evaluate to false, so return when any          // false condition is found.          if (result === false) { return false; }        }      }    }    return result;  },  /**   * Checks whether the value matches the requirements for this constraint.   *   * @param value   *   Either the value of a state or an array/object of constraints. In the   *   latter case, resolving the constraint continues.   * @param selector   *   The selector for this constraint. If undefined, there isn't yet a   *   selector that this constraint applies to. In that case, the state key is   *   propagates to a selector and resolving continues.   * @param state   *   The state to check for this constraint. If undefined, resolving   *   continues.   *   If both selector and state aren't undefined and valid non-numeric   *   strings, a lookup for the actual value of that selector's state is   *   performed. This parameter is not a State object but a pristine state   *   string.   *   * @return   *   true or false, depending on whether this constraint is satisfied.   */  checkConstraints: function(value, selector, state) {    // Normalize the last parameter. If it's non-numeric, we treat it either as    // a selector (in case there isn't one yet) or as a trigger/state.    if (typeof state !== 'string' || (/[0-9]/).test(state[0])) {      state = null;    }    else if (typeof selector === 'undefined') {      // Propagate the state to the selector when there isn't one yet.      selector = state;      state = null;    }    if (state !== null) {      // constraints is the actual constraints of an element to check for.      state = states.State.sanitize(state);      return invert(this.compare(value, selector, state), state.invert);    }    else {      // Resolve this constraint as an AND/OR operator.      return this.verifyConstraints(value, selector);    }  },  /**   * Gathers information about all required triggers.   */  getDependees: function() {    var cache = {};    // Swivel the lookup function so that we can record all available selector-    // state combinations for initialization.    var _compare = this.compare;    this.compare = function(reference, selector, state) {      (cache[selector] || (cache[selector] = [])).push(state.name);      // Return nothing (=== undefined) so that the constraint loops are not      // broken.    };    // This call doesn't actually verify anything but uses the resolving    // mechanism to go through the constraints array, trying to look up each    // value. Since we swivelled the compare function, this comparison returns    // undefined and lookup continues until the very end. Instead of lookup up    // the value, we record that combination of selector and state so that we    // can initialize all triggers.    this.verifyConstraints(this.constraints);    // Restore the original function.    this.compare = _compare;    return cache;  }};states.Trigger = function (args) {  $.extend(this, args);  if (this.state in states.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();    }  }};states.Trigger.prototype = {  initialize: function () {    var trigger = states.Trigger.states[this.state];    if (typeof trigger == 'function') {      // We have a custom trigger initialization function.      trigger.call(window, this.element);    }    else {      for (var event in trigger) {        if (trigger.hasOwnProperty(event)) {          this.defaultTrigger(event, trigger[event]);        }      }    }    // Mark this trigger as initialized for this element.    this.element.data('trigger:' + this.state, true);  },  defaultTrigger: function (event, valueFn) {    var oldValue = valueFn.call(this.element);    // Attach the event callback.    this.element.bind(event, $.proxy(function (e) {      var value = valueFn.call(this.element, e);      // Only trigger the event if the value has actually changed.      if (oldValue !== value) {        this.element.trigger({ type: 'state:' + this.state, value: value, oldValue: oldValue });        oldValue = value;      }    }, this));    states.postponed.push($.proxy(function () {      // Trigger the event once for initialization purposes.      this.element.trigger({ type: 'state:' + this.state, value: oldValue, oldValue: null });    }, this));  }};/** * This list of states contains functions that are used to monitor the state * of an element. Whenever an element depends on the state of another element, * one of these trigger functions is added to the dependee so that the * dependent element can be updated. */states.Trigger.states = {  // 'empty' describes the state to be monitored  empty: {    // 'keyup' is the (native DOM) event that we watch for.    'keyup': function () {      // The function associated to that trigger returns the new value for the      // state.      return this.val() == '';    }  },  checked: {    'change': function () {      return this.is(':checked');    }  },  // For radio buttons, only return the value if the radio button is selected.  value: {    'keyup': function () {      // Radio buttons share the same :input[name="key"] selector.      if (this.length > 1) {        // Initial checked value of radios is undefined, so we return false.        return this.filter(':checked').val() || false;      }      return this.val();    },    'change': function () {      // Radio buttons share the same :input[name="key"] selector.      if (this.length > 1) {        // Initial checked value of radios is undefined, so we return false.        return this.filter(':checked').val() || false;      }      return this.val();    }  },  collapsed: {    'collapsed': function(e) {      return (typeof e !== 'undefined' && 'value' in e) ? e.value : this.is('.collapsed');    }  }};/** * A state object is used for describing the state and performing aliasing. */states.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 states.State.aliases) {      this.name = states.State.aliases[this.name];    }    else {      break;    }  }};/** * Creates a new State object by sanitizing the passed value. */states.State.sanitize = function (state) {  if (state instanceof states.State) {    return state;  }  else {    return new states.State(state);  }};/** * This list of aliases is used to normalize states and associates negated names * with their respective inverse state. */states.State.aliases = {  'enabled': '!disabled',  'invisible': '!visible',  'invalid': '!valid',  'untouched': '!touched',  'optional': '!required',  'filled': '!empty',  'unchecked': '!checked',  'irrelevant': '!relevant',  'expanded': '!collapsed',  'readwrite': '!readonly'};states.State.prototype = {  invert: false,  /**   * Ensures that just using the state object returns the name.   */  toString: function() {    return this.name;  }};/** * Global state change handlers. These are bound to "document" to cover all * elements whose state changes. Events sent to elements within the page * bubble up to these handlers. We use this system so that themes and modules * can override these state change handlers for particular parts of a page. */$(document).bind('state:disabled', function(e) {  // Only act when this change was triggered by a dependency and not by the  // element monitoring itself.  if (e.trigger) {    $(e.target)      .attr('disabled', e.value)        .closest('.form-item, .form-submit, .form-wrapper').toggleClass('form-disabled', e.value)        .find('select, input, textarea').attr('disabled', e.value);    // 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) {    if (e.value) {      $(e.target).closest('.form-item, .form-wrapper').find('label').append('<span class="form-required">*</span>');    }    else {      $(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove();    }  }});$(document).bind('state:visible', function(e) {  if (e.trigger) {      $(e.target).closest('.form-item, .form-submit, .form-wrapper').toggle(e.value);  }});$(document).bind('state:checked', function(e) {  if (e.trigger) {    $(e.target).attr('checked', e.value);  }});$(document).bind('state:collapsed', function(e) {  if (e.trigger) {    if ($(e.target).is('.collapsed') !== e.value) {      $('> legend a', e.target).click();    }  }});/** * These are helper functions implementing addition "operators" and don't * implement any logic that is particular to states. */// Bitwise AND with a third undefined state.function ternary (a, b) {  return typeof a === 'undefined' ? b : (typeof b === 'undefined' ? a : a && b);}// Inverts a (if it's not undefined) when invert is true.function invert (a, invert) {  return (invert && typeof a !== 'undefined') ? !a : a;}// Compares two values while ignoring undefined values.function compare (a, b) {  return (a === b) ? (typeof a === 'undefined' ? a : true) : (typeof a === 'undefined' || typeof b === 'undefined');}})(jQuery);
 |