Quantcast
Channel: Issues for Drupal core
Viewing all articles
Browse latest Browse all 299604

When entire form is replaced by an Ajax response, Drupal.attachBehaviors() is called on an orphaned DOM node

$
0
0

Problem/Motivation

When an ajax-enabled form is set up to be completely replaced by a new copy of the form in the ajax response (using the replaceWith method), the Editor module attempts to reattach itself twice. The result is an error in the browser's JavaScript console, such as:

Uncaught The editor instance "edit-comment-body-0-value" is already attached to the provided element.

The underlying cause of this issue appears to be as follows. Consider the method Drupal.Ajax.prototype.success(). When one of the commands in the ajax response replaces the entire form, the following block of code triggers the functions that insert the new copy of the form into the DOM and attach the editor behaviors to the new form:

    for (var i in response) {
      if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) {
        this.commands[response[i].command](this, response[i], status);
        if (response[i].command === 'invoke'&& response[i].method === 'focus') {
          focusChanged = true;
        }
      }
    }

Then, near the bottom of Drupal.Ajax.prototype.success(), Drupal's ajax system attempts to reattach any behaviors that may not have been attached by the commands in the ajax response:
    // Reattach behaviors, if they were detached in beforeSerialize(). The
    // attachBehaviors() called on the new content from processing the response
    // commands is not sufficient, because behaviors from the entire form need
    // to be reattached.
    if (this.$form) {
      var settings = this.settings || drupalSettings;
      Drupal.attachBehaviors(this.$form.get(0), settings);
    }

According to the discussion in https://www.drupal.org/node/825318 , Drupal's ajax system requires it be possible to invoke behaviors from Drupal.attachBehaviors() multiple times without errors resulting. The discussion in that issue further points out that it is the responsibility of the Drupal.behaviorsattach() methods to avoid errors if they are called multiple times. The Editor module attempts to address this situation using jQuery.once:
  Drupal.behaviors.editor = {
    attach: function (context, settings) {
      // If there are no editor settings, there are no editors to enable.
      if (!settings.editor) {
        return;
      }

      $(context).find('[data-editor-for]').once('editor').each(function () {
        var $this = $(this);
        var field = findFieldForFormatSelector($this);
        // Etc...
      }
    }
  }

The first time the Drupal.behaviors.editor.attach() code is called, in response to the ajax replaceWith command, everything works fine. However, when this code is invoked a second time by Drupal.Ajax.prototype.success(), there is an error. To reiterate, the second time Drupal.behaviors.editor.attach() is called within Drupal.Ajax.prototype.success() is in this block of code:
    // Reattach behaviors, if they were detached in beforeSerialize(). The
    // attachBehaviors() called on the new content from processing the response
    // commands is not sufficient, because behaviors from the entire form need
    // to be reattached.
    if (this.$form) {
      var settings = this.settings || drupalSettings;
      Drupal.attachBehaviors(this.$form.get(0), settings);
    }

At this point in the code execution, this.$form is the DOM element for the original form, which the ajax replaceWith command has replaced with a new form. this.$form now refers to an orphaned DOM element that is no longer part of the global document. When this.$form is passed into Drupal.behaviors.editor.attach() as context, it is actually a separate, orphaned DOM element, which is not the copy on which Drupal.behaviors.editor.attach() operated previously. Therefore, calling jQuery once from that context will not prevent the code from operating on this.$form, because it is actually a separate copy of the form DOM element from the element that was already processed.

The reason this is a problem is the way that the Editor module's JavaScript finds the field to which the editor should be attached:

        var field = findFieldForFormatSelector($this);

The $this object in this case is a DOM element for the select drop-down field for picking the input format (“Basic HTML," etc.). The findFieldForFormatSelector() function uses the data-editor-for attribute of the select field to find the textarea element to which the editor should be attached:
  function findFieldForFormatSelector($formatSelector) {
    var field_id = $formatSelector.attr('data-editor-for');
    // This selector will only find text areas in the top-level document. We do
    // not support attaching editors on text areas within iframes.
    return $('#' + field_id).get(0);
  }

As the above code sample demonstrates, the findFieldForFormatSelector() function uses the element ID stored in the data-editor-for attribute to do a normal jQuery lookup of the textarea DOM element. Therefore, even though this function is processing two different copies of the form, since both copies can have the same value in the data-editor-for attribute of the select field for picking the input format, and since that value is used as an ID for a normal jQuery lookup of the textarea in the active DOM, the Drupal.behaviors.editor.attach() function can end up attempting to attach an editor to the same textarea field more than once.

Proposed resolution

The proposed solution is to prevent Drupal.behaviors.editor.attach() from operating on duplicate, orphaned copies of the form element. The patch file will change the code to read as follows:

  Drupal.behaviors.editor = {
    attach: function (context, settings) {
      // If there are no editor settings, there are no editors to enable.
      // Also check that the context still exists, and wasn't destroyed by an
      // ajax replaceWith command.
      if (!settings.editor || !document.contains(context)) {
        return;
      }

      $(context).find('[data-editor-for]').once('editor').each(function () {
        var $this = $(this);
        var field = findFieldForFormatSelector($this);
        // Etc...
      }
    }
  }

document.contains(context) will determine whether or not the DOM element stored in the context variable is actually part of the current document object or not. While testing this locally stepping through code in the Chrome developer console, this method returned the expected values at different points during the code execution:
document.contains(context)
true
document.contains(context)
false

I will attach a patch file shortly.

Remaining tasks

Please review the provided patch.

User interface changes

None.

API changes

None.

Data model changes

None.


Viewing all articles
Browse latest Browse all 299604

Latest Images

Trending Articles



Latest Images

<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>