Problem/Motivation

#2853208: [META] Determine best method ensure consistent theming of Off Canvas Tray explains the need to have the Off Canvas dialog look consistent when used in "admin" mode

It is very difficult to do this with a CSS reset but this is being explored in #2826722: Add a 'fence' around settings tray with aggressive CSS reset.

Another solution would be to use an iFrame. An iFrame could totally avoid CSS bleeding in from the sites default theme.
But it could also introduce other problems:

  1. Accessibility: what is the state of iFrame and Accessibility?
  2. Will not be able to use the regular method for dialog links. It doesn't make sense to load the iFrame element via AJAX because we just need the href for the link src attribute of the iframe element. Also even though there is dialog.js no where else in core uses the dialog system expect with AJAX dialogs(is this true??)
  3. Responsiveness

Proposed resolution

Use iFrame for dialog contents

Remaining tasks

Explore how an iFrame could be used.
Figure out if it is good idea.

User interface changes

API changes

Dialog link would not use class 'use-ajax'
No need for \Drupal\outside_in\Render\MainContent\OffCanvasRender
\Drupal\outside_in\Ajax\OpenOffCanvasDialogCommand

Data model changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tedbow created an issue. See original summary.

tedbow’s picture

Status: Active » Needs review
FileSize
12.59 KB

OK here is the first try at it.

I marked as do not test because I haven't updated the tests. But it does work, roughly

tedbow’s picture

Some more progress finding some tricking things. Will self review when uploaded.

tedbow’s picture

  1. +++ b/core/modules/outside_in/js/off-canvas.iframe.js
    @@ -0,0 +1,33 @@
    +      // Update href for all links not targeting the "_parent".
    +      $context.find('a[target!="_parent"]').each(function () {
    +        $(this).attr('href', $(this).attr('href') + '?_wrapper_format=drupal_dialog_offcanvas');
    +      });
    

    Any links that aren't explicitly targeting the _parent frame need to be appended to make sure they use the correct format.

    This uses OffCanvasRender. Basically this we only want to show the main content here. Also we need to remove the toolbar when we are inside the iframe.

  2. +++ b/core/modules/outside_in/js/off-canvas.iframe.js
    @@ -0,0 +1,33 @@
    +      $context.find('[data-dialog-renderer="offcanvas"]').each(function () {
    +        $(this).removeAttr('data-dialog-renderer');
    +        $(this).removeAttr('data-dialog-type');
    +
    

    Also we don't want any links to tray to use the iframe nested inside the original iframe.

  3. +++ b/core/modules/outside_in/js/offcanvas.js
    @@ -144,4 +144,73 @@
    +      if ($('.off-canvas-iframe').length) {
    +        // Already in off-canvas iframe do not open any links in the nested
    +        // iframe.
    +        // @see \Drupal\outside_in\Render\MainContent\OffCanvasRender::renderResponse
    +        return;
    +      }
    

    If we are already in the iframe don't attach event to handle opening links in the iframe. Don't want nested iframes.

  4. +++ b/core/modules/outside_in/js/offcanvas.js
    @@ -144,4 +144,73 @@
    +            var iframeTitle = document.getElementById('iframe-dialog').contentDocument.title;
    +            $('.ui-dialog-title').text(iframeTitle);
    

    This will make sure that the title of the dialog matches the title of the document inside the iframe.

    There is a little delay in the title showing up which is not ideal.

    @todo Need to figure out on server-side how to get rid of "* | SITE NAME" pattern.

  5. +++ b/core/modules/outside_in/outside_in.libraries.yml
    index 3a6e420..63711b7 100644
    --- a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
    
    +++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
    @@ -36,6 +36,7 @@ public function title(BlockInterface $block) {
    +    $form['#attributes']['target'] = '_parent';
    

    In the BlockEntityOffCanvasForm.php we have explicitly designate that the form should submit to the _parent frame. This is the default behaviour if we don't use the iframe.

    Should each form that will appear in the iframe have to set this? Or
    Can we handle this in \Drupal\outside_in\Render\MainContent\OffCanvasRender::renderResponse and somehow search through the whole content array and do this when #type = form?

    Are there any case where we would want a form to submit to the iframe?

  6. +++ b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php
    @@ -28,6 +28,17 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
    +    $main_content['#attached']['library'][] = 'outside_in/drupal.off_canvas_iframe';
    

    Adds library that keeps adds correct format.

  7. +++ b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php
    @@ -28,6 +28,17 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
    +    $main_content['#attributes']['class'][] = 'off-canvas-iframe';
    

    Adds a class to designate we are in the iframe. Used for JS now but could be used for styling.

    Should this be an attribute?

  8. +++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php
    @@ -49,7 +49,6 @@ public function linksDisplay() {
    -          'class' => ['use-ajax'],
    
    @@ -64,7 +63,6 @@ public function linksDisplay() {
    -          'class' => ['use-ajax'],
    
    @@ -82,7 +80,6 @@ public function linksDisplay() {
    -          'class' => ['use-ajax'],
    
    @@ -123,7 +120,6 @@ public function otherDialogLinks() {
    -            'class' => ['use-ajax'],
    

    Just updating offcanvas_test module to use iframe. Haven't actually fixed tests yet :(

So some new complications to think about!

nod_’s picture

I haven't looked to closely at the patch but from the start I'm not for an iframe solution for a couple of reasons:

  1. Most of the problems described in #4 we had with the overlay. And the 1k lines of JS that came with the module to deal with them is gone for a reason.
  2. Duplication of many scripts. There will be JS inside the canvas iframe, which means the same JS will be loaded. Just like that the JS is doubled on the page. Knowing that there is about 600kb of JS loaded on the front page when logged in (quickedit/toolbar/etc), When the iframe opens, there is about 1.3MB of JS on the page! this is nuts.
  3. Will need to introduce a new API or something to reference elements within the iframe, can't jQuery it like before, that'll introduce quite a bit of overhead (from the overlay, about a few hundred lines of JS at least).

Purely on the performance aspect that's a no-go for me.

The overlay was one thing, but at least it was a whole page where no direct interactions with the opened page was supposed to happen, this is not the case here. If you have an ajax call on the main page, attachBehaviors will not be called inside the iframe. So you need to initialize things twice or introduce something new somewhere. If a contrib module makes content appear in the off canvas thing, and expect the content to be quick-editable, what happens? I don't want to have attachbehavior look for iframes or be monkey-patched by the outside-in module to do it.

tedbow’s picture

@nod_ thanks for the feedback from your experience with Overlay. I am inclined to agree with you. I just through Overlay's JS, WOW
I think in general if we stick with the regular dialog system we are in for less surprises when we try to use this with other parts of core and contrib.

I think if there anyway we could another way to work we should do. Probably something along the lines of #2826722: Add a 'fence' around settings tray with aggressive CSS reset.

tkoleary’s picture

@nod

The overlay was one thing, but at least it was a whole page where no direct interactions with the opened page was supposed to happen, this is not the case here. If you have an ajax call on the main page, attachBehaviors will not be called inside the iframe. So you need to initialize things twice or introduce something new somewhere.

Yes, but that is simply a description of the nature of this challenge which is 'how do we achieve administrative interactions that directly manipulate rendered content on the page in a fluid and intuitive way'.

Overlay never did anything like that. It seems obvious to me that a solution that does would be more complex and would require more code. Remember though that this is a performance hit that is *only* incurred by an administrator and not by a site visitor.

The trade off of having a much more direct path to editing almost everything in a site with immediate preview is well worth that cost IMO.

Having said all that, if you have up your sleeve a clever way to achieve the same goal without iFrame I would be very eager to hear it. :)

nod_’s picture

we do have control over the css we could prefix all css rules from a specific frontend-admin library with some id at least that'll boost the rules specificity. Or add !important to everything during preprocess

Wim Leers’s picture

we do have control over the css we could prefix all css rules from a specific frontend-admin library with some id at least that'll boost the rules specificity. Or add !important to everything during preprocess

That's exactly the CSS nightmare that the iframe-based solution would avoid :)

#5:

  1. Most JS is not relevant in the Off-Canvas Tray. We don't need Quick Edit or the Toolbar there, for example. Nor do we need Contextual Links.
  2. Why? JS in the main window should not be altering the DOM in the Off-Canvas Tray. Communication between frames can happen via window.postMessage() IIRC.

To be clear: I'd rather not use iframes either. But given that we can't use Shadow DOM, I don't see how else we can arrive at a sane, workable solution, that doesn't require every single bit of CSS to be duplicated, hence making Off-Canvas Tray-specific CSS an absolute nightmare. That's the only reason I even proposed this.

tkoleary’s picture

@Nod

There is another benefit of iFrame that has not been mentioned, Without iFrame, special CSS needs to be written for content in the tray to make it behave like it's in a narrow viewport. With iFrame the narrow viewport media queries for mobile also apply to the tray (when it's used at it's default narrow width). It essentially gives us the equivalent of an element query.

tedbow’s picture

re #5.3 and @Wim Leers' response

Why? JS in the main window should not be altering the DOM in the Off-Canvas Tray. Communication between frames can happen via window.postMessage() IIRC.

I think eventually though we will want JS in the tray to altering DOM elements in the main page. The original Outside In designs I think provide optimistic feedback editing items in the tray. For example editing the Branding block and updating the site name. Ultimately it would be nice if the title on the page updated as you typed. We aren't there yet but probably we will want be able to do things like that.

To me it seems like it will be harder to do such things if the elements aren't in the same frame in the same DOM.

tkoleary’s picture

One other option we have not suggested yet AFAICR is putting the tray styling in the javascript and injecting it inline, which will always win out in specificity.

Wim Leers’s picture

#11:

I think eventually though we will want JS in the tray to altering DOM elements in the main page.

But you can still have that. Ensure you have different JS in the main frame and in the Settings Tray iframe, and have them communicate via window.postMessage(). That probably even results in a cleaner separation of concerns.

EDIT: or I guess https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API.


#12: How are you going to gather the appropriate CSS to inject? You'd need to parse CSS, and run every single CSS selector through JS, scoped to the Settings Tray root element, and then generate style attributes for each selector's style rules.

tkoleary’s picture

#12: How are you going to gather the appropriate CSS to inject? You'd need to parse CSS, and run every single CSS selector through JS, scoped to the Settings Tray root element, and then generate style attributes for each selector's style rules.

Yeah, I was thinking of some kind of pre-render but @mattgrill explained the huge pitfalls of that.

GrandmaGlassesRopeMan’s picture

Some thoughts,

#5

Duplication of many scripts. There will be JS inside the canvas iframe, which means the same JS will be loaded. Just like that the JS is doubled on the page. Knowing that there is about 600kb of JS loaded on the front page when logged in (quickedit/toolbar/etc), When the iframe opens, there is about 1.3MB of JS on the page! this is nuts.

Looking at an obviously contrived example, loading the 'Main Navigation' edit form in the tray executes another 276 requests with aggregation turned off. I don't think we are going to want to re-render an entire form and all the weight that comes with it. U+1F44D

#5, #9, #11

I think @tedbow is correct. At some point in the future we'd like to have optimistic feedback that can escape the confines of the iframe. Even if we used window.postMessage(), we would still want to provide a consistent interface that other developers could take advantage of. Additionally, window.postMessage() is only supported starting with IE8 which may or may not be of concern.

However could we do something like the following.

const iframe = jQuery('<iframe />')
  .appendTo('#wrapper') // The tray.
  .contents();
  
iframe.find('head')
  .append($('<style>body { background: black; color: red; }</style>')); // Inject a link to the actual stylesheet. 
iframe.find('body')
  .append(jQuery('<p>sample text</p>')); // This would be the ajax'd html of the form.

I think this allows us to keep the form encapsulated in a frame, but we can also just load (via ajax) the form html, preventing the duplicate loading of scripts.

Wim Leers’s picture

loading the 'Main Navigation' edit form in the tray executes another 276 requests with aggregation turned off

Reproduced. But you forgot to mention they're all served from the browser's cache!

At some point in the future we'd like to have optimistic feedback that can escape the confines of the iframe.

How would an iframe prevent that?

we would still want to provide a consistent interface that other developers could take advantage of

What would that consistent interface look like? I think it's fair to guess that it'd be an event. And for that even to be consistent, it'd have to tell the Settings Tray/Outside In to trigger an event, so that all Outside In events are handled the same. At which point… Outside In might as well be using window.postMessage() inside the iframe, that message would be received inside the main frame and would trigger an event there.
So, I really don't see what the problem is with that.

only supported starting with IE8 which may or may not be of concern.

That's not a concern.

So: I'm not an expert in this, nor am I fan of iframes. But I've only seen pretty thin arguments against iframes so far.

P.S.: in testing that, I noticed that \Drupal\outside_in\Block\BlockEntityOffCanvasForm::form() is called three times when opening a menu block in the Settings Tray! That's a major bug right there, I'm afraid…

tkoleary’s picture

@cosmicdreams suggested the alternative of using webcomponents.js to polyfill shadow dom—apart from drawbacks of yet another javascript library others already voiced—In looking through the documentation for webcomponent.js I discovered that it cannot effectively polyfill all of the CSS properties of web components without resorting to, you guessed it, an iFrame!

See https://github.com/webcomponents/webcomponentsjs/blob/master/src/ShadowC...

cosmicdreams’s picture

Hi gang: thanks for bringing this issue to my attention here @tkleary.

I just wanted to update everyone on the progress that has gone into web components in case you were not aware. About a year or two ago, the major browser vendors starting meeting regular to discuss how to properly implement web components in a way that all browsers can be comfortable with. What was produced from those discussions is V1 of the four main web standards that comprise this Web Components concept (Templates, Custom Elements, Shadow Dom, and HTML Imports).

Natively, most browsers support templates (I'm looking at you IE 11). Support for Custom Elements and Shadow Dom is either underway for most browsers to on the list of things to do / considering (yes even for IE Edge). I think it's a good bet to expect that native support will come eventually. (I'm being optimistic).

Until then a polyfill does exist. You may have seen it before : https://github.com/WebComponents/webcomponentsjs/tree/v1

The link above points to the matured version of this set of polyfills. Whearas before, the polyfill was a pretty large file that was loaded for all the browsers, they've now created a tiny js file to detect what level of support for Web Components your browser has and then load the appropriate polyfill. As a result: Complete support of the standards can be enabled through the download of a payload varying between < 7 kb -> ~ 30kb (referencing minified / compressed file sizes). That get the library size into the ballpark of jQuery. For a library, that may not be needed for anonymous users.

I'll report back soon. But I'm also working with the folks who make the ui_patterns module to introduce automatic discovery of these 3rd party components so that your Drupal site can have smart ways to populating them with data.

Fundamentally though, I really think the Shadom Dom support that Web Components provide is specifically designed to solve the problem of allowing these Admin elements to stand alone without having outside influence bleed in.

tkoleary’s picture

@cosmicdreams

I personally am very excited about these advances, but as others have said better than me, it seems like core experimental is not the place for this just yet.

I am going to really dig in on this and start testing it out in other more bleeding edge experiments I'm working on though, and I'd really like to talk to you in more depth about it if you're going to be in Baltimore.

tedbow’s picture

Status: Needs review » Fixed

Marking this as fixed because we explored it.
CSS reset seems to be the way to go. #2826722: Add a 'fence' around settings tray with aggressive CSS reset.
See discussion
here, #2826722, and #2853208: [META] Determine best method ensure consistent theming of Off Canvas Tray

Status: Fixed » Closed (fixed)

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

tedbow’s picture

Component: outside_in.module » settings_tray.module

Changing to new settings_tray.module component. @drpal thanks for script help! :)