Working on a patch for entity_embed, I found that the behavior attach / detach in editor.js doesn't fully pass the incoming "context" down it callstack.

It ends up doing $(selector), instead of the recommended $(selector, "context for the attach/detach").

This is good enough in most real-life cases, but breaks when the behaviors are being attached to an element in an iframe - e.g, hem, a CKEditor in iframe mode). $(selector) cannot find content within an iframe, while $(selector, someElementInTheIframe) works as expected. Then, $(selector) being null, some code later dies when trying to call a method on the result.

Definitely an edge case, but entity_embed lets you embed a node in 'full' mode, which includes a comment form... You naturally don't really expect that to render "fine", but with the current editor.js, the JS errors badly break the functionality.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yched’s picture

Status: Active » Needs review
Issue tags: +JavaScript
FileSize
3.3 KB

Patch.

nod_’s picture

Status: Needs review » Needs work

The syntax for contextual selectors is $(context).find(selector). We don't use the second argument for context.

As for the code itself, it needs to be looked into, the comment of findFieldForFormatSelector suggest it's not the right place to make the change. Would need to check whether the comment or the code is more correct :p

slashrsm’s picture

Status: Needs work » Needs review
FileSize
3.31 KB
455 bytes

First part of #2 fixed.

yched’s picture

@nod_

the comment of findFieldForFormatSelector suggest it's not the right place to make the change

I don't think I get what you mean. How does the comment suggest that ?

findFieldForFormatSelector() is merely a glorified element selector ("find the DOM textarea pointed by the $formatSelector's data-editor-for attribute"), used as a helper for attach/detach.
Thus, as any code in attach/detach, it has to be scoped within a given context (which is the context that was used to find the $formatSelector that is being worked on in the first place) ?

Taking a step back - the specific logic here is the following :
- Some behavior needs to be attached to a pair of elements (a "format select" + the associated textarea). The association is representend by the format select having a data attribute pointing to its textarea's ID.
- The current attach logic is:
Find the format selects present the "attach context"
Then look for the associated textareas not specifically in that same context, but (implicitly, by the absence of a context for the jQ selector used) in the toplevel document.
This breaks when the format select is not in the toplevel document to begin with.

So either:

1) (current patch) The textarea needs to be looked for in the same "context" than the select.
This is consistent with the general rule that "all DOM manipulations within attach/detach have to be scoped to the context of the attach/detach"
But it implies that the select and its textarea are always shipped alongside (you never handle an "attach context" that contains one and not the other). That sounds like a reasonable assumption, but I'm not 100% sure...

2) Or, the textarea needs to be looked for in the same "document" than the select:

var field_id = $formatSelector.attr('data-editor-for');
return $formatSelector.parents('body').find('#' + field_id).get(0);

That looks a bit weird without an explanation though.
Maybe, going up a little less, up to a known common parent ? '.text-format-wrapper' ?

3) Or, the code needs to be bullet-proofed for the fact that maybe the associated textarea will not be found where it is currently looked for (in the toplevel document)

Ideally, we'd do 2), I guess. Question is: can we trust the select and its textarea will have a known common parent (that doesn't depend on theme verbosity & classes...) ?

Wim Leers’s picture

editor.js indeed only works for the top-level document, because, well, none of us expected to have a WYSIWYG editor within a WYSIWYG editor :D

It sounds very fascinating, but are you sure you even want a CKEditor instance inside an iframe of another CKEditor instance? I wonder if CKEditor even will work correctly.

I'd say we basically don't want to call Drupal.attachBehaviors() inside WYSIWYG iframes?

(I hope I didn't misinterpret what is described here, because my comment feels so … weird, that I can't shake the feeling I'm just misunderstanding completely!)

jcisio’s picture

Some (or most async) media player require Drupal.attachBehaviors() to replace the placeholder with the real element. When this player is inside an iframe (basically we want a wysiwyg-alike in an iframe editor), we need Drupal.attachBehaviors() in an iframe too. E.g. we want an embedded tweet in CKEditor in iframe mode.

yched’s picture

No, the idea is indeed to attach behaviors within CK (iframe or divarea)

The case here is a combination of several things :

- Entity_embed lets you embed "a rendered entity in a given view mode" inside a body field, and have a faithful preview in CK. This is intended as "the unified D8 solution for embedding media in textareas", whatever the various underlying media data model (Media, Scald...).
- The API for rendering an entity (entity_view($entity, $view_mode)) doesn't let you specify "BTW, this one is going to be rendered in a wysiwig, so you might want to output things a little differently". This also wouldn't play nice with the render cache. So the HTML you get in the wysiwyg is strictly the same as the one used in front end.
- Thinking of "media" use cases, for example, there are totally cases where that HTML might need some JS massaging - which are typically implemented with behaviors.

Taking actual examples from a recent D7 client case (the one I showed you in Amsterdam :-) :
- picturefill needs to scan the new content for picture tags, or images do not show on browsers without native picture support
- an image lazy-load script needs to scan the new content for imgs, or images do not show
- we also had some some custom "contextual-links"-like code to allow editing the media in a popup, from the embedded representation in the wysiwig; that's implemented with behaviors too - because behaviors are our tool for this.

So, running behaviors in there makes a lot of sense IMO. That's what the patch in https://github.com/drupal-media/entity_embed/pull/103 does.

Then, if you embed a node in a view mode that displays the comment field - yeah, you get a comment textare displayed inside your CK, and editor.js behavior trying to run on it ...
Weird case, and of course we don't expect to have that textarea to get its own CK... but at least we'd like the JS not to break :-)

Wim Leers’s picture

Ok, so doing Drupal.attachBehaviors() in a WYSIWYG editor does have some valid use cases.

But wouldn't it make sense to then exclude Drupal.behaviors.editor.attach() from being executed? That'd avoid the recursive WYSIWYG editing! And would solve the "bug" in editor.js — which I'd argue is not really a bug, because we landed WYSIWYG editing in core, not Inception WYSIWYG editing :)

yched’s picture

But wouldn't it make sense to then exclude Drupal.behaviors.editor.attach()

I've been toying with that idea as well - but how would we do that ? Drupal.attachBehaviors() is currently all or nothing.

I guess maybe entity_embed could use some JS foo to manually redefine a couple selected Drupal.[some_component].attach() to do
"if (context is within a CK) {return;}; call the original implementation" ? You can do such things in JS right ?
(not really clean though, open to other suggestions...)

yched’s picture

Also - we intend to support <picture> in our core image CK plugin at some point, right ? Then, the question of running behaviors in CK content will arise in core too ?

Well, or I guess that specific case can manually trigger picturefill() calls on the new content instead of going through the generic behaviors attach. Entity_embed is much more generic, and so cannot know specifically which behaviors to run.

yched’s picture

Regarding the "bug" in editor.js - what about this ?

- keeps the current lookup logic (look for the textarea in the top document, don't write code to support the weird case of textareas in iframes / CK inception),
- just safeproof the attach / detach code to do nothing in case no textarea was found
- side-effect : patch finds the textarea once, and then passes the [format select, textarea] pair to the various helpers, instead of having to find the textarea again each time

Wim Leers’s picture

So your code currently does this:

Drupal.attachBehaviors(target, response.settings || ajax.settings || drupalSettings);

(FYI: that can be simplified to response.settings || ajax.settings at the very least — see Drupal.attachBehaviors()'s body.)

What about changing it to this:

// Run all Drupal behaviors, except those for editor.module, because we end
// up with recursive/inception WYSIWYG editing otherwise!
var tmp = Drupal.behaviors.editor;
delete Drupal.behaviors.editor;
Drupal.attachBehaviors(target, response.settings || ajax.settings);
Drupal.behaviors.editor = tmp;

Works perfectly AFAICT. :)

That way, all behaviors work, except the one you don't want to, because it'd cause inception WYSIWYG.

yched’s picture

@Wim : thanks, did that (https://github.com/yched/entity_embed/commit/1f0a4bc36b16a5a8cb912475c0b...).

I still think safeproofing the way the [select, textarea] pair is passed around, as proposed in #11, would make sense, but no biggie.

Wim Leers’s picture

I'm not entirely convinced by #11, but it's definitely better than earlier patches.

nod_, what do you think?

nod_’s picture

Status: Needs review » Needs work

So #11 doesn't declare or use the context parameter of findFieldForFormatSelector is that wanted? Otherwise I agree, it does make sense to pass around the field at that level.

Wim Leers’s picture

So you also think #12 is an adequate solution for yched's main problem?

yched’s picture

Status: Needs work » Needs review
FileSize
3.87 KB
1.21 KB

So #11 doesn't declare or use the context parameter of findFieldForFormatSelector is that wanted

Indeed, those were leftovers. Removed.

+ Adds the missing doc for the new "field" param added to changeTextEditor()

Wim Leers’s picture

I'm sorry, but unless #12 is not an acceptable solution, I don't really see what problem #11/#17 are solving? Could you explain that? If there can be CKEditor instances in child documents, I can understand why thanks the earlier comments, but now I don't understand. Sorry for asking you to explain it again, but I really want to thoroughly understand why we're doing this :)

yched’s picture

@Wim Leers:

In current HEAD, editor.js behaviors attach/detach:
- finds all DOM elements with the '.editor' class,
- assumes they have a 'data-editor-for' attribute, containing the ID of an element present in the top-level document (does not intend to support textareas in iframes)
- and then does stuff on that pair of elements, confident that all the conditions above were met and that the pair was found.

The script breaks, messing with the rest of the JS execution on the page, when that pair was not found for whatever reason.
That could be :
1) because the '.editor' element is completely unrelated (that class is not really namespaced and is easy to clash with)
2) because some code tried to attach/detach behaviors on an element in an iframe - which is not a completely crazy thing to do (and there could be other cases than CK iframes ? For example, our popups / modals currently happen to be divs, but there are other, iframe-based approaches to popups ?)

For the above, and independently of which actual cases it choses to support (CK in a CK [obviously not], CK in a non-CK iframe),
I think editor.js should gracefully degrade to "not for me, just do nothing" on the cases it choses *not* to support, rather than telling you you did a forbidden thing by breaking JS execution.

The latest patches try to do that : only "do the stuff" if the two elements were actually found, for the cases it intends to support.
Then, the question of "what exactly do we want to support" (CKeditor in a non-CK iframe ?) can be discussed separately.

Writing the above, I notice that the patch does not fix case 1) above (an '.editor' element without a 'data-editor-for' attribute).
Could be that $(context).find('.editor') would need to be refined to $(context).find('.editor[data-editor-for]') ? I thought the trend for "behavior markers" in the DOM was to move away from marker classes and rely on data- attributes instead, for that matter ?

yched’s picture

I thought the trend for "behavior markers" in the DOM was to move away from marker classes and rely on data- attributes instead, for that matter ?

Grepping for "$(context).find('[" in core leads to:
states.js : behavior based on [data-drupal-states]
vertical-tabs.js : ... [data-vertical-tabs-panes]
node.preview.js : ... [data-drupal-autosubmit]
quickedit.js : ... [data-quickedit-field-id] / [data-quickedit-entity-id]
navbar.js : ... [data-drupal-nav-tabs]

So it does seem like a common, if not fully unified, approach ? Even though we are a bit inconsistent on how exactly to namespace those marker attributes (data-drupal-[component], data-[component]...)

nod_’s picture

Indeed, the inconsistency is because of time. quickedit and vtabs were converted before we introduced the "drupal" namespace on data attributes.

So changing to [data-editor-for] as the selector would be much better than the class yes.

About #12, while it works, it's kind of ugly. I'm not really happy with messing with the behavior object like that. I'd much rather add an argument to attachbehavior that takes the list of behavior you want to run (it would also possibly solve the behavior ordering issue too…).

nod_’s picture

yched’s picture

@nod_ : if the 'drupal-' namespace is the most recent convention, then data-drupal-editor-for ?

nod_’s picture

Yes but it's kind of scope creep here. Should be an other issue to change all of them.

Wim Leers’s picture

#19: Ahhh! Now I understand. Thank you so much for explaining that so completely! :)

#23/#24: I'm totally fine with doing the data- attribute conversion in this issue.

Code review:

  1. +++ b/core/modules/editor/js/editor.js
    @@ -33,9 +35,8 @@
    -  function changeTextEditor($formatSelector, activeFormatID, newFormatID) {
    +  function changeTextEditor($formatSelector, field, activeFormatID, newFormatID) {
    

    We can drop the $formatSelector argument entirely if we're passing in field anyway.

  2. +++ b/core/modules/editor/js/editor.js
    @@ -146,6 +148,9 @@
    +        if (!field) {
    +          return;
    +        }
    

    This can go away once the selector is converted to using the data- attribute only.

yched’s picture

re #25:

1. We can drop the $formatSelector argument entirely [in changeTextEditor()] if we're passing in field anyway.

The last line of the function sets an attribute on the $formatSelector, so it seems we still need it ?

2. if (!field) {return} can go away once the selector is converted to using the data- attribute only.

I don't think so :
- "Using the data- attribute only" takes care of case #19.1 (a DOM element with the 'editor' class that is just a false-positive).
- That if (!field) return is what takes care of case #19.2 : do nothing if findFieldForFormatSelector() could not find a textarea that we intend to support (in current code, that's a textarea in the top-level document).

Wim Leers’s picture

#26.1: True, for getting/setting data-editor-active-text-format, which is currently stored on the <select> element. But I don't see any reason why we can't move that onto the field?

#26.2: Right, sorry. I keep getting confused by that.

yched’s picture

Attached patch ties the behavior to the data-editor-for attribute instead of the .editor class.

yched’s picture

re @Wim #27 - Putting data-editor-active-text-format on the textarea rather than the select :
Oh, right. Looks like a slightly larger refactor, but could be done I guess. Will look into it.

yched’s picture

Updated patch does as suggested in #27.1 : simplifies changeTextEditor() by storing the data-editor-active-text-format on the field (textarea) rather than on the select.

+ Since the field knows its currently active format, no need to pass that current format as a separate param ?

--> patch does:

-  function changeTextEditor($formatSelector, field, activeFormatID, newFormatID) {
+  function changeTextEditor(field, newFormatID) { 
yched’s picture

- Slightly clearer var name within changeTextEditor() (s/activeFormatID/previousFormatID)

- While I'm in there, simplify if (cond) {...} else if (!cond) {...}
to if (cond) {...} else {...} :-)

jhedstrom’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
rpayanm’s picture

nod_’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
yched’s picture

Status: Needs work » Needs review
FileSize
11.11 KB

Reroll

nod_’s picture

Get an eslint error at: { field: field }

should be {field: field}

Other than that, looks RTBC to me.

yched’s picture

Fixed :-)

nod_’s picture

Status: Needs review » Reviewed & tested by the community

All good now, thanks.

quickedit still works.

The last submitted patch, 35: 2362637-editor_js_attach_detach-35.patch, failed testing.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs reroll

This issue is a normal bug fix, and doesn't include any disruptive changes, so it is allowed per https://www.drupal.org/core/beta-changes. Committed 465dc04 and pushed to 8.0.x. Thanks!

  • alexpott committed 465dc04 on 8.0.x
    Issue #2362637 by yched, slashrsm, rpayanm: Editor.js attach/detach...

Status: Fixed » Closed (fixed)

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