When submitting a form via AJAX, such as in a CTools Modal, Drupal's AJAX script keeps a reference to the form around to be able to attach behaviors to it again after running all commands returned from the server. Problems arise when the form itself is removed by one of the commands. In this case, the command was
modal_dismiss. The form is removed from the document, but it's not completely gone yet because Drupal still holds a reference to it. That reference is then passed on as
Drupal.attachBehaviors(this.form, settings) (often with all of
Drupal.settings since closing a modal generates no new settings command), tricking all module behaviors on the page into thinking the form elements are to be used. Modules attaching their behaviors must now check whether the
context argument they're given is actually part of the document, and if any elements they may be looking for as outside that context can be reached at all.
This issue was found while debugging a reopened/repurposed issue in Wysiwyg module - see
I've posted a workaround patch for Wysiwyg in the previously mentioned issue, another for fixing this at the CTools level, but I would prefer if Core didn't try to attach behaviors to elements no longer part of the document in the first place. ;)
The steps I reproduced this with:
- Start with a Drupal installation having Wysiwyg, CTools, and Panels (enable Panel nodes and the In-Place Editor), and an editor library. I'll use CKEditor (3) here since it gives an easy to trace error.
- Assign an editor profile to any format, say Filtered HTML.
- Create a Panel page, any layout, make sure the In-Place Editor is enabled and view the node.
- Click "Customize this page" at the bottom of the window.
- Click the "+" icon to add new pane to any area
- Click "New custom content" and the WYSIWYG editor should appear.
- Type anything into the editor and click "Finish"
- An error is thrown by the editor after the modal closes:
Uncaught TypeError: Cannot call method 'getEditor' of undefined
- Breaking on the error, walk up the stack until you get to ajax.js, line 410. This call gets passed a reference to the form which CTools just removed from the IPE modal as part of the modal_dismiss command just a few lines above.
Why the error?
- Modal opens -> Form is loaded -> behaviors get attached (using settings from the AJAX response) -> Wysiwyg finds a format selector and attaches the editor inside a
.once('wysiwyg',...)call. Fine so far
- Form is submitted -> Behaviors are told to detach (serialize) -> Behaviors detach again (unload) -> Wysiwyg removes the editor and the 'wysiwyg-processed' class -> The modal and form are removed. Still doing fine.
- Drupal triggers attaching behaviors on the lingering form reference again (this time without any settings from the AJAX response, but they've been merged into
Drupal.settings) -> Wysiwyg uses
.once('wysiwyg', ...)and finds the same format selector, thanks to
context-> Wysiwyg passes the id of the field associated with that format selector to the editor library -> The editor library attempts to locate the field by id -> OOOPS! Not so fine anymore...
Fixing the cause:
To prevent these problems, it would be nice if Core checked that the form it's about to attach behaviors to is actually part of the document first. This is quite easy to do and should have no side-effects. Any behavior which previously didn't throw errors would still have had all their hard work undone as soon as the form reference went out of scope anyway.
Note: I've not looked into D8 or D6 yet.