When aggregating our JS, we hit errors on panels admin pages. Example:

Error: Uncaught ReferenceError: id is not defined

From this:
for (id in Drupal.settings.CTools.dependent) {

The problem is that id has not been defined. By itself, this script will work fine, but we have enabled aggregation, and another script must be setting strict mode, as this line throws an error in such a case due to the variable not having been declared.

The solution is to declare variables in JS before using them:

for (var id in Drupal.settings.CTools.dependent) {

I will apply a patch to the first comment.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Jaypan created an issue. See original summary.

Jaypan’s picture

Jaypan’s picture

Oops, bad file references in patch. Uploading new patch.

joelpittet’s picture

Version: 7.x-1.15 » 7.x-1.x-dev
Status: Active » Fixed

Thanks @Jaypan, I've committed this to the dev branch to be included in the next release.

Here's a documentation page for reference https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statem...

Jaypan’s picture

Gracias :)

joelpittet’s picture

Status: Fixed » Needs review
FileSize
3.66 KB

Actually before I committed I found some other instances and it doesn't relate to the documentation but these others are the same as the patch.

Could you double check I got them all?

joelpittet’s picture

I used this regex to find the others for ?\(.* in

Jaypan’s picture

To be honest, I always run my scripts through JSlint. It's a hassle to learn all their rules, but once you do, it makes for much cleaner JavaScript, that is syntaxically cross-browser friendly, in strict mode. Of course, if you make a call to a function one browser supports but another doesn't, that won't work.

For example, I cleaned up dependent.js like this:

/**
 * @file
 * Provides dependent visibility for form items in CTools' ajax forms.
 *
 * To your $form item definition add:
 * - '#process' => array('ctools_process_dependency'),
 * - '#dependency' => array('id-of-form-item' => array(list, of, values, that,
 *   make, this, item, show),
 *
 * Special considerations:
 * - Radios are harder. Because Drupal doesn't give radio groups individual IDs,
 *   use 'radio:name-of-radio'.
 *
 * - Checkboxes don't have their own id, so you need to add one in a div
 *   around the checkboxes via #prefix and #suffix. You actually need to add TWO
 *   divs because it's the parent that gets hidden. Also be sure to retain the
 *   'expand_checkboxes' in the #process array, because the CTools process will
 *   override it.
 */

/*global
  jQuery, Drupal
*/
/*jslint
  white:true, this, browser:true, es6:true
*/

(function ($, Drupal) {
  "use strict";

  Drupal.CTools = Drupal.CTools || {};
  Drupal.CTools.dependent = {};

  Drupal.CTools.dependent.bindings = {};
  Drupal.CTools.dependent.activeBindings = {};
  Drupal.CTools.dependent.activeTriggers = [];

  Drupal.CTools.dependent.inArray = function(array, search_term) {
    return ($.inArray(search_term, array) !== -1);
  };

  Drupal.CTools.dependent.autoAttach = function() {
    // Clear active bindings and triggers.
    $.each(Drupal.CTools.dependent.activeTriggers, function (i) {
      $(Drupal.CTools.dependent.activeTriggers[i]).unbind('change.ctools-dependent');
    });

    Drupal.CTools.dependent.activeTriggers = [];
    Drupal.CTools.dependent.activeBindings = {};
    Drupal.CTools.dependent.bindings = {};

    if (!Drupal.settings.CTools) {
      return;
    }

    // Iterate through all relationships.
    $.each(Drupal.settings.CTools.dependent, function (id) {
      // Test to make sure the id even exists; this helps clean up multiple
      // AJAX calls with multiple forms..

      // Drupal.CTools.dependent.activeBindings[id] is a boolean,
      // whether the binding is active or not.  Defaults to no.
      Drupal.CTools.dependent.activeBindings[id] = 0;
      // Iterate through all possible values
      $.each(Drupal.settings.CTools.dependent, function (bind_id) {
                // This creates a backward relationship.  The bind_id is the ID
        // of the element which needs to change in order for the id to hide or become shown.
        // The id is the ID of the item which will be conditionally hidden or shown.
        // Here we're setting the bindings for the bind
        // id to be an empty array if it doesn't already have bindings to it
        if (!Drupal.CTools.dependent.bindings[bind_id]) {
          Drupal.CTools.dependent.bindings[bind_id] = [];
        }
        // Add this ID
        Drupal.CTools.dependent.bindings[bind_id].push(id);
        // Big long if statement.
        // Drupal.settings.CTools.dependent[id].values[bind_id] holds the possible values

        var trigger_id;
        if (bind_id.substring(0, 6) === 'radio:') {
          trigger_id = "input[name='" + bind_id.substring(6) + "']";
        }
        else {
          trigger_id = '#' + bind_id;
        }

        Drupal.CTools.dependent.activeTriggers.push(trigger_id);

        var val;
        if ($(trigger_id).attr('type') === 'checkbox') {
          $(trigger_id).siblings('label').addClass('hidden-options');
        }

        var getValue = function(item, trigger) {
          if (!$(trigger).size()) {
            return;
          }

          if (item.substring(0, 6) === 'radio:') {
            val = $(trigger + ':checked').val();
          }
          else {
            switch ($(trigger).attr('type')) {
              case 'checkbox':
                // **This check determines if using a jQuery version 1.7 or newer which requires the use of the prop function instead of the attr function when not called on an attribute
                if ($().prop) {
                  val = $(trigger).prop('checked');
                }
                else {
                  val = $(trigger).attr('checked');
                }

                if (val) {
                  $(trigger).siblings('label').removeClass('hidden-options').addClass('expanded-options');
                }
                else {
                  $(trigger).siblings('label').removeClass('expanded-options').addClass('hidden-options');
                }

                break;
              default:
                val = $(trigger).val();
            }
          }
          return val;
        };

        var setChangeTrigger = function(trigger_id, bind_id) {
          // Triggered when change() is clicked.
          var changeTrigger = function () {
            val = getValue(bind_id, trigger_id);

            if (val === null) {
              return;
            }

            $.each(Drupal.CTools.dependent.bindings[bind_id], function (i) {
              id = Drupal.CTools.dependent.bindings[bind_id][i];
              // Fix numerous errors
              if (typeof id === 'string') {
                // This bit had to be rewritten a bit because two properties on the
                // same set caused the counter to go up and up and up.
                if (!Drupal.CTools.dependent.activeBindings[id]) {
                  Drupal.CTools.dependent.activeBindings[id] = {};
                }

                if (val && Drupal.CTools.dependent.inArray(Drupal.settings.CTools.dependent[id].values[bind_id], val)) {
                  Drupal.CTools.dependent.activeBindings[id][bind_id] = 'bind';
                }
                else {
                  delete Drupal.CTools.dependent.activeBindings[id][bind_id];
                }

                var len = 0;
                $.each(Drupal.CTools.dependent.activeBindings, function () {
                  len += 1;
                });

                var $original = $('#' + id);
                if ($original.is('fieldset') || $original.is('textarea')) {
                  var object = $original.parent();

                  if (Drupal.settings.CTools.dependent[id].type === 'disable') {
                    if (Drupal.settings.CTools.dependent[id].num <= len) {
                      // Show if the element if criteria is matched
                      // **This check determines if using a jQuery version 1.7 or newer which requires the use of the prop function instead of the attr function when not called on an attribute
                      if (typeof $().prop === 'function') {
                        object.prop('disabled', false);
                        object.addClass('dependent-options');
                        object.children().prop('disabled', false);
                      }
                      else {
                        object.attr('disabled', false);
                        object.addClass('dependent-options');
                        object.children().attr('disabled', false);
                      }
                    }
                    else {
                      // Otherwise hide. Use css rather than hide() because hide()
                      // does not work if the item is already hidden, for example,
                      // in a collapsed fieldset.
                      // **This check determines if using a jQuery version 1.7 or newer which requires the use of the prop function instead of the attr function when not called on an attribute
                      if (typeof $().prop === 'function') {
                        object.prop('disabled', true);
                        object.children().prop('disabled', true);
                      }
                      else {
                        object.attr('disabled', true);
                        object.children().attr('disabled', true);
                      }
                    }
                  }
                  else {
                    if (Drupal.settings.CTools.dependent[id].num <= len) {
                      // Show if the element if criteria is matched
                      object.show(0);
                      object.addClass('dependent-options');
                    }
                    else {
                      // Otherwise hide. Use css rather than hide() because hide()
                      // does not work if the item is already hidden, for example,
                      // in a collapsed fieldset.
                      object.css('display', 'none');
                    }
                  }
                }
              }
            });
          };

          $(trigger_id).bind('change.ctools-dependent', function() {
            // Trigger the internal change function
            // the attr('id') is used because closures are more confusing
            changeTrigger(trigger_id, bind_id);
          });
          // Trigger initial reaction
          changeTrigger(trigger_id, bind_id);
        };
        setChangeTrigger(trigger_id, bind_id);
      });
    });
  };

  Drupal.behaviors.CToolsDependent = {
    attach: function () {
      Drupal.CTools.dependent.autoAttach();

      // Really large sets of fields are too slow with the above method, so this
      // is a sort of hacked one that's faster but much less flexible.
      $("select.ctools-master-dependent")
        .once('ctools-dependent')
        .bind('change.ctools-dependent', function() {
          var val = $(this).val();
          if (val === 'all') {
            $('.ctools-dependent-all').show(0);
          }
          else {
            $('.ctools-dependent-all').hide(0);
            $('.ctools-dependent-' + val).show(0);
          }
        })
        .trigger('change.ctools-dependent');
    }
  };
}(jQuery, Drupal));

This passes JSlint. Note it's not tested.

joelpittet’s picture

I'm confident the changes we made in the patch won't break anything and are easy to commit with minimal review time. To keep the scope low on this but get all the bits in one shot could you do a quick pass to see if the changes look good from your end?

joelpittet’s picture

Linters are great but better to start with from the beginning, we are using eslint in D8 core.

Jaypan’s picture

Well, I run JSlint just locally, before committing any code to DO. It's something I do separately.

I gave the patch from #6 a go-over. There is nothing that could break anything in there.

Jaypan’s picture

Status: Needs review » Reviewed & tested by the community

  • joelpittet committed 2234984 on 7.x-1.x
    Issue #3063176 by Jaypan, joelpittet: JavaScript - undeclared variables...
joelpittet’s picture

Status: Reviewed & tested by the community » Fixed

Thanks for having a look over it. I've committed it to the dev branch (for real). That's a good practice to get into to run a linter over everything. I think the testbot people (@mixologic and company) are looking to do that with CSS and JS for all drupal projects.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Chris Matthews’s picture