If a form contains a wysiwyg editor and a file field, the wysiwyg editor will be duplicated when an ajax submission fails validation. The screenshots below show TinyMCE, but the duplication occurs for some of the other editors as well.

Case 1: No file fields, validation failed. Everything is fine in this case.

Case 2: Contains a file field, validation failed. Buttons have been duplicated.

I attached the module that I used to create the screenshots above. You can comment/uncomment these lines in the module to see the behavior shown above. You'll need to have wysiwyg configured for full_html for the test module to work.

// ---------------------------------------------------------------------------------------------------------------------
// Uncomment the following lines and submit the form without filling in the name field. You should see duplicated
// wysiwyg buttons appear.
// ---------------------------------------------------------------------------------------------------------------------
//    'resume' => array(
//      '#type' => 'managed_file',
//      '#title' => t('Resume'),
//    ),
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

TwoD’s picture

Status: Active » Closed (won't fix)

This is not a bug in Wysiwyg module, but a consequence of how #ajax works, and there's nothing we can do about it in Wysiwyg.

The #ajax functionality is not intended to update the entire form, only parts of it. Or rather, it's not intended to actually replace the <form> element itself. At the least, I can't find an example of where that's actually done in a module. The examples.module always uses a different element in the 'wrapper' key than the form itself.

Replacing the form element itself means the internal reference to the entire form in ajax.js line 408 is no longer valid and is a lingering reference to the form that was before it got replaced by the insert command a few lines above.

Because Wysiwyg received orders to detach itself from the document before the elements were replaced, that version of the form no longer has the "wysiwyg-processed" class on the select box for picking the format, just like the new version of the form won't have.

The "insert" AJAX command triggers Drupal.attachBehaviors() on the inserted content (usually parts of the form) to make sure editors and other behaviors remain functional after AJAX updates. That prompts Wysiwyg to insert the editor like it normally would. To be sure all parts of the form remain functional ajax.js, just after line 408, will trigger Drupal.attachBehaviors() on the stored reference to the entire form after all AJAX commands have been executed.

That old form reference gets handed down to Wysiwyg as the context in which to [re]attach editor instances to any format selection box which, as noted earlier, no longer has the "wysiwyg-processed" class. The editor will always fetch a "fresh" reference to the current document before attaching itself, so it gets added to the new version of the form created by the "insert" AJAX command. Thus, there will be two editor instances rendered for the same field. The button duplication happens because the editor settings passed back with the AJAX command will in this case be merged with the existing settings for this field/format, causing button duplication in the toolbar and a few other oddities.

Working around all this is pretty easy by adding a div container inside the form element and replacing that instead.

function testform_form($form, $form_state) {
  return array(
    'simplewrapper' => array(
      '#type' => 'container',
      '#prefix' => '<div id="simplewrapper_div">',
      '#suffix' => '</div>',
      'name' => array(
        '#type' => 'textfield',
        '#title' => t('Name'),
        '#required' => TRUE,
      ),
      'resume' => array(
        '#type' => 'managed_file',
        '#title' => t('Resume'),
      ),
      'about' => array(
        '#type' => 'text_format',
        '#title' => t('About'),
        '#default_value' => '',
        '#format' => 'full_html'
      ),
      'submit' => array(
        '#type' => 'submit',
        '#value' => t('Submit'),
        '#submit' => array('testform_submit'),
        '#ajax' => array(
          'callback' => 'testform_ajax_submit',
          'event' => 'click',
          'wrapper' => 'simplewrapper_div',
        ),
      ),
    ),
  );
}

function testform_submit($form, &$form_state) {
  $form_state['msg'] = 'Thanks for submitting.';
}

function testform_ajax_submit($form, $form_state) {
  $errors = form_get_errors();
  if (count($errors) == 0) {
    return '<div>' . $form_state['msg'] . '</div>';
  }
  else {
    return $form['simplewrapper']; // Only return the wrapper
  }
}
rylowry@gmail.com’s picture

Thanks for the detailed explanation. Even though its a won't fix, your comment was very helpful. Thanks for taking the time to look at this and provided some info about it.