Problem/Motivation

During the initial development cycle of #1137920: Fix toolbar on small screen sizes and redesign toolbar for desktop, it was noted that the behavior of the overlay in relation to the Toolbar is not ideal. When the toolbar tray is oriented vertically, it's not yet known what the ideal reaction of the overlay should be.

Overlay over the vertical toolbar tray.

overlay position over the vertical toolbar tray

Proposed resolution

None yet.

Remaining tasks

None yet.

User interface changes

Yes there will be.

API changes

None expected.

Followup: #1958246: Update JS documentation to follow Doxygen format (remove curly braces)

Original report by jessebeach

CommentFileSizeAuthor
#91 overlay-toolbar-overlap.jpg45.68 KBdamontgomery
#76 interdiff.txt10.81 KBnod_
#75 core-js-toolbar-displace-1847084-75.patch32.75 KBnod_
#70 Screenshot_3_24_13_2_35_PM.png72.83 KBjessebeach
#70 Screenshot_3_24_13_2_35_PM.png77.87 KBjessebeach
#70 Screenshot_3_24_13_2_30_PM.png85.69 KBjessebeach
#70 Screenshot_3_24_13_2_31_PM.png83.56 KBjessebeach
#70 toolbar-overlay-1847084-70.patch33.94 KBjessebeach
#70 interdiff_68-to-70.txt24.3 KBjessebeach
#69 mockup.png30.22 KBjessebeach
#68 core-js-offsets-1847084-68.patch21.08 KBnod_
#63 toolbar-overlay-1847084-63.patch22.74 KBjessebeach
#63 interdiff_61-to-63.txt11.86 KBjessebeach
#61 toolbar-overlay-1847084-61.patch23.32 KBtrawekp-1
#61 interdiff_58-to-61.txt6.79 KBtrawekp-1
#59 interdiff_58-to-59.txt2.95 KBtrawekp-1
#59 toolbar-overlay-1847084-59.patch24.69 KBtrawekp-1
#58 displacing-elements-2.png38.37 KBjessebeach
#58 overlay.png40.6 KBjessebeach
#58 toolbar-overlay-1847084-58.patch24.47 KBjessebeach
#58 interdiff_54-to-58.txt4.32 KBjessebeach
#58 testswarm-displace-tests.txt8.45 KBjessebeach
#57 toolbar-overlay-1847084-55.patch22.14 KBtrawekp-1
#57 interdiff_54_to_55.txt6.93 KBtrawekp-1
#56 interdiff_52-to-54.txt4.65 KBjessebeach
#56 toolbar-overlay-1847084-54.patch22.75 KBjessebeach
#52 toolbar-overlay-1847084-52.patch21.51 KBjessebeach
#52 interdiff_51-to-52.txt9.68 KBjessebeach
#51 toolbar-overlay-1847084-51.patch18.32 KBtrawekp-1
#48 toolbar-overlay-1847084-48.patch19.42 KBjessebeach
#46 vertical-toolbar-overlay-displace-1847084-46.patch8.28 KBtrawekp-1
#45 vertical-toolbar-overlay-displace-1847084-45.patch8.27 KBtrawekp-1
#31 displace.txt2 KBtrawekp-1
#29 toolbar-overlay-1847084-29.patch9.9 KBjessebeach
#27 vertical-toolbar-overlay-1847084-27.patch2.57 KBtrawekp-1
#26 Screenshot_3_3_13_3_58_PM-2.png58.61 KBjessebeach
#25 sidebar bug.png102.46 KBmkadin
#20 FF-win7.png111.36 KBtarekdj
#20 Chrome-win7.png89.54 KBtarekdj
#20 Safari-win7.png92.7 KBtarekdj
#20 IE9-win7.png74.76 KBtarekdj
#20 vertical-toolbar-overlay-1847084-20.patch2.57 KBtarekdj
#16 overlay-vertical-toolbar.png56.32 KBDavid_Rothstein
#14 drupal-1847084-vertical-toolbar-overlay.patch1.72 KBLewisNyman
overlay-on-top-of-toolbar.png55.3 KBjessebeach
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

moshe weitzman’s picture

I floated a proposal that seems to have good initial support. I propose that after we get inline editing, we move Overlay to Contrib. The main use case for overlay will be handled in a better way.

Shyamala’s picture

Issue tags: +toolbar-followup

+toolbar-followup

Bojhan’s picture

Shouldn't it just go to the right of the vertical toolbar? I just see a bug here.

@moshe His suggestion to me feels, out of scope and not related to this bug but simply a feature request.

xjm’s picture

Yeah, the toolbar being stuck under the overlay in when it's vertical is simply a bug, and "kill overlay" is out of scope for fixing it. :) (Also, inline editing doesn't work as the response for clicking a contextual link doodad to configure something, so -1.)

nod_’s picture

The kill switch for overlay on screens smaller than 640px is broken. The toolbar behavior executes after the overlay behavior and messes things up. if the Overlay behavior is after the toolbar's it works. Haven't dug into why.

ry5n’s picture

It appears that the toolbar is conflicting with the JS that disables the overlay for screen sizes less than 640px. Reloading an overlay URL with the viewport < 640px will still load the overlay. _nod did some digging and indicated to me on IRC that it might be Toolbar behaviours running after the Overlay behaviour has been called.

Bojhan’s picture

Title: Resolve the layout conflicts between the Toolbar and Overlay modules » Vertical toolbar doesn't work with overlay
Category: task » bug
Priority: Normal » Major

That you can't use vertical toolbar, with the overlay is a major bug - almost critical. Since you essentially can't use it anymore.

mbrett5062’s picture

Maybe I am missing something entirely here, but AFAICT the current functionality, with 'overlay' overlaying the toolbar tray is perfectly fine. I access the overlay to do something after clicking it in the tool tray, say 'appearance'. Then I do what I need and when finished close the overlay and move on to my next task. Why is this a problem? Can someone tell me why I would need access to the toolbar tray while I am in an overlay doing some administrative task. Or are we just asking for a quicker way to get out of one overlay to another?

carwin’s picture

The issue could be solved by modifying the z-index of the toolbar, which would force it above the overlay, but then we would have the reverse problem where the overlay wouldn't be accessible because it's hidden behind the toolbar.

One solution might be to go ahead and do the z-index adjustment to get the toolbar above the overlay and then add some code at this smaller breakpoint that automatically closes the toolbar tray after a selection has been made.

Thoughts?

@mbrett5062 - It's really just a usability issue. Many folks don't manually close the overlay and at this viewport width, with a touchscreen, the "x" to close the overlay is a bit difficult to land on properly.

mbrett5062’s picture

The overlay closes upon completion of the task at hand, it is not a normal action to close it manually. If need be just change some CSS to re-size the close button on smaller screens. I still don't see a need to do anything else. An overlay is exactly what it says on the box. It overlays the page, moving attention away from the page, to concentrate on the task at hand. It works, leave it to do it's job. Just my opinion of course.

David_Rothstein’s picture

It closes automatically if you got there via a contextual link (for example), but not normally. If you are just browsing around your site doing a few administrative tasks, you really need to be able to browse around, not be forced to manually click the "close" button each time you want to go somewhere new.

http://drupal.org/node/1175694 and http://drupal.org/node/1427940 have lots of videos, etc., of people using the overlay to accomplish various tasks. Browsing around via the toolbar and closing often are very common, as I recall.

David_Rothstein’s picture

In the original issue (#1137920: Fix toolbar on small screen sizes and redesign toolbar for desktop), I believe @webchick suggested the problem with the overlay could be fixed by simply removing the toggle option for overlay users (which I think really translates to removing it for everyone). So people on wide screens would be forced to use the horizontal toolbar, and people on narrow screens (where the overlay is currently turned off anyway) would use the vertical toolbar.

I am not sure if that makes sense or not, but if you look at usage statistics of http://drupal.org/project/usage/admin_menu vs. http://drupal.org/project/usage/admin it seems pretty clear to me that people prefer horizontal toolbars, at least when their screen is wide enough (and those statistics don't even take into account people using the D7 core toolbar, which of course is horizontal too). So I'm not sure the vertical toolbar on a wide screen is a common enough use case that Drupal core needs to provide it.

Bojhan’s picture

All of these ideas, do not make sense - we just place the overlay next to the vertical toolbar. That's all, I dont see why we need to make it more complex.

LewisNyman’s picture

Shame on you all, upsetting Bojhan.

Patch! :-)

LewisNyman’s picture

Status: Active » Needs review
David_Rothstein’s picture

FileSize
56.32 KB

Have mercy! :)

Going off what @jessebeach wrote in http://drupal.org/node/1137920?page=1#comment-6756324 and http://drupal.org/node/1137920?page=1#comment-6757716 it seemed the more obvious approach here was going to have very complicated issues; perhaps that's not the case anymore.

The above patch seems to work pretty well at various widths; however I doubt I tested all the places where it was expected to cause trouble.

There is also a crazy issue with a detached hovering scrollbar (in Firefox at least) on the right side of the screen:
overlay-vertical-toolbar.png

rteijeiro’s picture

Status: Needs review » Needs work

I could confirm the detached vertical scrollbar issue of #1847084-16: Measure/track displacing elements better + provide change events for them (fix overlay + toolbar) on Chrome, Opera and Safari too.

dman’s picture

I can +1 that this was UI WTF for me as I was clicking around on a new install.
The buttons up the top looked like menu links (admin-menu style) and I didn't recognize them as toggles or anything. They all have different behaviours it seems.

Anyway, I had an overlay open, then thought I'd like to hit the menu again. Clicking menu didn't do anything apparent until I noticed it popping in and out in the obscured background. Confusing, looked broken, definitely needed a fix.

So far I can confirm that this patch does work much better. It's functional.

Seeing as it seems to achieve its purpose via css only, I don't expect functional issues (no unit test can be written AFAIK)

TLDR: +1

echoz’s picture

.overlay-element is an iframe, and in this patch it's changing % widths introduces it's own scrollbars.

tarekdj’s picture

Attached patch fixes the problem. Tested successfully on FF, Chrome, safari, IE9 and chrome (Win 7).

tarekdj’s picture

Status: Needs work » Needs review

changing status

Bojhan’s picture

Looks good to me

jessebeach’s picture

Issue tags: +sprint, +Spark

adding Spark and sprint tags

mkadin’s picture

Status: Needs review » Needs work

I do thin Bojhan's solution is the best one to solve this, overlay should just appear to the right of the sidebar.

I tested this in chrome (on ubuntu). Works well for wide screens but as the window gets thin, the overlay drops down and some empty space shows up at the top right of the toolbar.

mkadin’s picture

FileSize
102.46 KB

Forgot attachment

jessebeach’s picture

Thanks LewisNyman and tarekdj for the patches. I think the approach represented in the patch in #20 has legs, but as mkadin points out in #24, the CSS responsible for positioning the overlay container doesn't respond well at narrow screen widths.

Screen shot of the toolbar open with a vertical tray. The overlay is open. There is a gap above the overlay about 200px.

The approach in the latest patch is a great start. I'd like to suggest we continue down this route and further abstract the displacement code in overlay so that overlay isn't referring so directly to toolbar dimensions, like in this example below.

+++ b/core/modules/overlay/overlay-parent.jsundefined
@@ -488,8 +488,13 @@ Drupal.overlay.eventhandlerAlterDisplacedElements = function (event) {
     // Prevent displaced elements overlapping window's scrollbar.
     var currentMaxWidth = parseInt($(this).css(maxWidthName), 10);
+    // If toolbar is visible add it's width to maxWidth
+    var toolbarWidth = 0;
+    if ($(".tray.vertical.active").width()) {
+      var toolbarWidth = parseInt($(".tray.vertical.active").width(), 10);
+    }
     if ((data.drupalOverlay && data.drupalOverlay.maxWidth) || isNaN(currentMaxWidth) || currentMaxWidth > maxWidth || currentMaxWidth <= 0) {
-      $(this).css(maxWidthName, maxWidth);
+      $(this).css(maxWidthName, maxWidth + toolbarWidth);
       (data.drupalOverlay = data.drupalOverlay || {}).maxWidth = true;

A module like toolbar should be able to register that it has visual components that require displacement on the top and left/right. A module like overlay could register that it needs to be displaced on the top, left and right. In contrast, a component like sticky table headers would only be interested in top displacement.

Currently the displacement code lives in the overlay javascript. I'd like to see us generalize these behaviors to the Drupal object, so that screen real estate isn't managed in any one particular module.

trawekp-1’s picture

#26: This is a good idea but it is quite complicated. As you can see Toolbar has class on "body" that adds a left margin, if it would work like you describe then if there was a other module that would display something similar to toolbar on screen but for differeren purpose then it would have to register it's own displacement and so on..

That's great if that would work but to achieve that all those margins set on body could not be set using CSS but JS that could read from Drupal object those displacement settings.

---

According to patch in #20. I've found out that making a overlay container positioned absolute would work better. Now it is positioned relatively but this can not work.

Imagine that window height is 900px, there is no guarantee that #page-wrapper has 900px height. That means that #overlay-container might be lower than 900px from the top. That means that margin-top: -100% might not push the #overlay-container to top of the window, because it calculates the negative margin from body height, not #page-wrapper. Initial position of container depends on #page-wrapper height not on body height.

First alternative is to add max-height: 100% to #page-wrapper but we can't be sure that #page-wrapper would even exist in custom theme.
Second alternative is to add "position: relative" to body and to position #overlay-container absolutelty (as it is originally).

I'm attaching a patch based on patch from #20.

VVVi’s picture

Status: Needs work » Needs review

changing status

jessebeach’s picture

Status: Needs review » Needs work
FileSize
9.9 KB

#27

This is a good idea but it is quite complicated. As you can see Toolbar has class on "body" that adds a left margin, if it would work like you describe then if there was a other module that would display something similar to toolbar on screen but for differeren purpose then it would have to register it's own displacement and so on..

Oh, I do agree. It is quite complicated! Nonetheless, I thought I'd give it a spin. We've got a few weeks to really take a hard look at integrating the features we just landed. I'd love to see us clean up some D7 cruft and make the new features play nice as well.

So I thought, what if we have a utility called Drupal.displace. It is responsible for finding the .data-offset-{edge} classes, getting their dimensions and triggering an event when those values change. A module like overlay just needs to listen for that event and update its dimensions based on the values it gets from the utility. The responsibility of measuring the elements is no longer located in Overlay.

By moving this code into a utility, we start to build an approach that could help us address this issue as well: #1827318: Fix tableheader placement which is broken in the current D8 ToolBar patch.

What I've got in this patch is still pretty rough. I just wanted to establish a draft framework, details to be cleaned up.

What this buys us is that overlay no longer refers to toolbar. So if in contrib later, someone builds a toolbar replacement, they don't have to override overlay as well to get the same or similar offset behavior.

trawekp-1’s picture

This is really interesting. But do we really want to rely on classes and events? What do you think about adding a object that has displacement method and all page elements making displacement of overlay?

I think that creating new event types that should be triggered on body while resize is tricky to notice for developers. If it would be stored in Drupal object then you just open inspector and you see that there is a displacement object that stores all this stuff. I know that all those things would be documented but I guess object would be easier to understand.

*EDIT*
Sorry for my "it's complicated". I'm starting with helping with core and I though that we are looking for simple bug-fix. Obviously I prefer the way jessebeach suggested.

trawekp-1’s picture

FileSize
2 KB

I had big problems with creating a patch. Patch from #29 has completly different overlay code so I gave up.

I attach my vision of displace.js. "getDisplace" method is easily overrideable (if objects would be stored with ID or something).

jessebeach’s picture

#30

Sorry for my "it's complicated". I'm starting with helping with core and I though that we are looking for simple bug-fix. Obviously I prefer the way jessebeach suggested.

No need to be sorry! I agree, it is complicated. Sometimes we need complicated, sometimes we don't. I like to explore the complicated and see if we can pull something simple out of it. I'm glad you're starting with core. There are a lot of patterns and sub-systems buried in here and it just takes time to get familiar with them. I learned by working with others who have more experience than me. I like discussing options with folks to see if there's anything I haven't considered yet so if you're up for it, we can explore some options here as well.

What problems did you have with the patch in #29? Applying or creating an updated patch from the changes? Are you sure you applied the patch in #29 to the latest 8.x branch? Not the branch with your patch from #27 already applied. I just tried the patch in #29 applied to the latest 8.x commit (a3064d2020d442266b3bcdf70029775a72f67dae) and it applies cleanly.

I like using JavaScript classes, especially when we need to store state on a per-instance basis. From your patch in #31, I assume we would decorate elements like the toolbar container and the tray container with a .displace class. These we would create a Displace class instance for each of these elements.

In your model, how do you foresee us handling these situations?

  1. Letting a module know what elements on the screen have displacement values.
  2. A screen resize.
  3. Displace elements resizing or toggle visibility.

For (1), I assume that a module calls Drupal.displace.getOffsets(true[, '{edge}']). I am, however, limited to the values at the precise moment I call this function, so we might run into race conditions if elements are still drawing or if the order of operations is not maintained.

For resizing and displacement element dimension changes, we need a way to update interested elements that they need to reposition themselves. The best way I could come up with to do this is to dispatch an event with the most current offset values and let other modules listen for these events and react as they want.

Attaching a handler to the document is a way to capture bubbled events: http://www.quirksmode.org/js/events_order.html. It's also a singular clearing house for custom events. We use this pattern a lot in core, especially to pass state information between modules without those modules referring directly to each other's methods.

Contextual module listening for an Edit module event
$(document).on('drupalEditModeChanged.contextual', toggleEditMode);

We always name these events with the pattern drupalPropertyEventtype.

You're right, it does require some documentation to know about these events. My personal philosophy is that contrib should be able to get by without knowing anything about these sub-systems. But when you do, you can build complex administration features that interact well with the subsystems or even completely replace them.

By going with a utility approach to managing element displacement, we'll need to address the following tasks

  1. Update the sticky table headers to use this utility.
  2. Clean up and optimize the displace utility code.
  3. Further clean out overlay. Its eventhandlerAlterDisplacedElements is setting max widths and heights in a way that's no longer compatible with this new approach.
trawekp-1’s picture

In your model, how do you foresee us handling these situations?

  • Letting a module know what elements on the screen have displacement values.
  • A screen resize.
  • Displace elements resizing or toggle visibility.

1. Either a class on element or adding an instance of displace object to Drupal.settings.displace.elements
2. I'd suggest recalculating all offsets every time window resizes
3. Provide a method in displace object to recalculate offsets. This could be also resolved by custom event. Default resize event is only triggered on window.

What problems did you have with the patch in #29? Applying or creating an updated patch from the changes? Are you sure you applied the patch in #29 to the latest 8.x branch? Not the branch with your patch from #27 already applied. I just tried the patch in #29 applied to the latest 8.x commit (a3064d2020d442266b3bcdf70029775a72f67dae) and it applies cleanly.

I had newest branch 8.x. I don't know why but my overlay-parent.js had differences in lines than the one in your patch. That was strange. I guess it was my mistake, I haven't got enough time to investigate that so I attached only js file.

I cannot judge what method is better cause I'm the new one here. If you say that custom events is ok and it is used inside core then I truly belive you. I asked cause the more I understand the more I can help.

Anyway I have a question: Since data attr is only valid for HTML5 what do you suggest for XHTML and so?

jessebeach’s picture

You could use XHTML5 to validate the data attributes: http://www.w3.org/TR/html5/introduction.html#html-vs-xhtml

#33

2. I'd suggest recalculating all offsets every time window resizes
3. Provide a method in displace object to recalculate offsets. This could be also resolved by custom event. Default resize event is only triggered on window.

Ok, cool, I think we did land on the same approach. Here's the resize handler in my patch from #29 (admittedly still very rough). It should only be triggering drupalViewportOffsetChange if the offset values change, not on every window resize event.

  /**
   * Registers a resize hanlder on the window.
   */
  Drupal.behaviors.drupalDisplace = {
    attach: function (context, settings) {
      $('body').once('drupalDisplace', function (index, element) {
        findDisplacingElements();
        $(window).on('resize.drupalDisplace', displace);
      });
    }
  };

/**
   * Informs listeners of the current offset dimensions.
   */
  function displace() {
    findDisplacingElements();
    calculateOffsets();
    $(document).trigger('drupalViewportOffsetChange', offsets);
  }

And to get the offsets outside of the event layer, we have:

/**
   * Returns the current offset dimensions.
   */
  displace.getOffsets = function (refresh) {
      if (refresh) {
        calculateOffsets();
      }
      return offsets;
  };
// Invoked like so.
var currentOffsetValues = Drupal.displace.getOffsets(true);

Also, when applying a patch, you can use the --reject flag to at least apply the parts that aren't in conflict, for example:

git apply ~/path/to/some.patch --reject

trawekp-1’s picture

Well, what about XHTML 1.0, HTML 4 and others? Validator won't pass data attribute in those.

I agree that everything shouldn't be calculated on every window resize event but if there would be a dynamic width, like "width: 20%" then width of element would change on every window resize.

jessebeach’s picture

We're building to a doctype of <!DOCTYPE html>. I've not heard anyone mention the need to support validation of HTML4.X or XHTML 1.X.

trawekp-1’s picture

Hmm.. that's a suprise you're not considering compatibility with older doctypes. I won't argue with that. This is only another reason to stop using XHTML 1.x! What about percentage width?

jessebeach’s picture

What about percentage width?

We need to recalculate the displacement offsets on window resize, but only changes need to be broadcast. So if we compare the fresh calculations against the stored values from the previous resize, we can determine if they changed and only fire drupalViewportOffsetChange when they do. Sound good?

trawekp-1’s picture

Yeah, but in the matter of performance it would work the same as mine idea so I won't consider it as a progress. I must spent some time to find something better. We don't want to overdose with calculations while resize, especially on mobile devices with slower cpu's.

I hope it is the last thing I ask but what would happen if someone wants to change dimensions in his theme using CSS? He would have to change data attribute as well? That might not be comfortable for themers. We must think about such things if we want to do such framework like displace.

jessebeach’s picture

We don't want to overdose with calculations while resize, especially on mobile devices with slower cpu's.

That shouldn't be an issue since resizes happen rarely on small devices where the viewport is often simply maximized and unresizable.

what would happen if someone wants to change dimensions in his theme using CSS?

We should be fine there too, since we're getting the calculated dimensions of the elements to determine the displacements. As long as we get the width/height. padding and border sizes for any given box that will be considered part of the displacement value for an edge, we should be fine. The only way we could really know if an element changed size outside of a screen resize is to track DOM mutation events, but those aren't well supported at all yet. So any module that alters its view (like the toolbar and its tray) will need to call some method of displace to trigger a reassessment of the displacement values and possible trigger a drupalViewportOffsetChange event if they've changed. I don't like that part, but it seems hard to avoid if we can't simply monitor DOM elements for attribute changes.

trawekp-1’s picture

That shouldn't be an issue since resizes happen rarely on small devices where the viewport is often simply maximized and unresizable.

Have you seen Ubuntu Touch? One of its features is that you can have browser on the left part of screen and something else on the right. The middle edge can be moved so window resize happens. I guess in future there will be more devices with such feature.

Hopefully all calculations are made after 'resize.drupalDisplace' so now it's not the case.

I've been thinking about improving your patch. Here are some things i thought about:

  • Provide a way of calling 'resize.drupalDisplace' only for specified element. It would be better if we could not only tell that element box has changed but also tell displace api what element has changed. Having this feature calculations would be done only for changed element. If we stored element previous offset then we would know what change it was and we could immediately adjust offsets.
  • Do we really have to call "findDisplacingElements()" every time displacement happens? Why isn't this called only in behavior using context? Isn't it additional work every time "resize.drupalDisplace" is called?
  • Another thing is calculating an offset. Now it is taken from .data-offset-{edge} and if this is not existing then only width is taken. First of all instead of "width()", "outerWidth()" should be called because it includes paddings.

    Anyway I think that .data-offset-{edge} is way better than width() or outerWidth() because it gives modules a opportunity to set offset by itself. But what would happen if we had two toolbars on the left and one would be under another? In this case calculations would be wrong. I'm aware that this example is weird but this is going to be an api not only for toolbar module so we should think about such options.

    To prevent mistakes in calculations we could use something like .offset().left + .outerWidth() for left edge in example. This guarantees us that we always get proper offset no matter how elements are positioned.

Am I right with those things? I'd be happy to implement some solutions for those problems but before I'll start it's better to know if it makes any sense.

jessebeach’s picture

Have you seen Ubuntu Touch? One of its features is that you can have browser on the left part of screen and something else on the right. The middle edge can be moved so window resize happens. I guess in future there will be more devices with such feature.

I haven't seen it, but I think I know what you're talking about.

Check out this issue for a list of the followups. You're welcome to add a feature request for this idea. I like it.
#1846970: [meta] Responsive Toolbar follow-ups

Some thoughts on your suggestions:

Provide a way of calling 'resize.drupalDisplace' only for specified element. It would be better if we could not only tell that element box has changed but also tell displace api what element has changed. Having this feature calculations would be done only for changed element. If we stored element previous offset then we would know what change it was and we could immediately adjust offsets.

I think I understand. What I'd like to avoid is having the Overlay ask Drupal.displace for the size of a specific element. This is an end-run around the API. Overlay shouldn't need to worry about referring to DOM elements directly or knowing anything about their classes or ids.

Do we really have to call "findDisplacingElements()" every time displacement happens? Why isn't this called only in behavior using context? Isn't it additional work every time "resize.drupalDisplace" is called?

You're quite right, we shouldn't need to do this. The only alternative I can come up with is any module, like Toolbar, that has DOM elements that will displace other elements, will need to proactively tell Drupal.displace what elements it should take into account. I'm not thrilled about that option, because it's more brittle. Take a shot at it and let's see what your ideas look like in code.

Another thing is calculating an offset. Now it is taken from .data-offset-{edge} and if this is not existing then only width is taken. First of all instead of "width()", "outerWidth()" should be called because it includes paddings.
Anyway I think that .data-offset-{edge} is way better than width() or outerWidth() because it gives modules a opportunity to set offset by itself. But what would happen if we had two toolbars on the left and one would be under another? In this case calculations would be wrong. I'm aware that this example is weird but this is going to be an api not only for toolbar module so we should think about such options.

To prevent mistakes in calculations we could use something like .offset().left + .outerWidth() for left edge in example. This guarantees us that we always get proper offset no matter how elements are positioned.

Yes, we should be using outerWidth and taking a displaced element's offset into account as well. Calculating right and bottom displacement will take a little more code on top of that. Don't forget about RTL languages like Hebrew, too.

I think you're in a good place to start coding it up!

trawekp-1’s picture

I haven't seen it, but I think I know what you're talking about.

Check out this issue for a list of the followups. You're welcome to add a feature request for this idea. I like it.
#1846970: [meta] Responsive Toolbar follow-ups

I'm not following you, what feature request are you talking about? I got lost.

I think you're in a good place to start coding it up!

I'll do the best i can!

jessebeach’s picture

I'm not following you, what feature request are you talking about? I got lost.

Oh, I meant if you'd like to add functionality to allow a user to resize a vertical try, you should open an issue for that.

trawekp-1’s picture

I've added TableHeader integration with displace.js. Now TableHeader listens for drupalViewportOffsetChange and as a result invokes Drupal.displace.getOffsets(true).

I did not change displace.js, as I need to get familiar with other core libraries. There is a lot of details that need to be considered to make this API stable.

Caching

I don't know if we should cache total or per-element offsets.

I think that best for performance would be per-element caching. When element triggers and event that its box has changed then we'd just adjust total offsets based on change of single element, we'd skip recalculating all elements. But I'm not sure if this is needed, because there won't be a lot of elements marked as displace element. I think it would not be more than 3. In this case there is no sense in caching offsets per-element.

So assuming that we have only a few elements then all elements providing displacements would only trigger an event to tell displace api to recalculate total offset. If total offset changes then displace api triggers "drupalViewportOffsetChange" on document then all interested modules can react on change.

Window resize

Also I think that there is no need for "resize" handler in displace.js. We should only recalculate offsets when something tells us to. Window resize is not equal to change of offsets. Modules will tell us when to recalculate offsets.

Calculations

Should we rely on data-offset-{edge}? I know it is good, because modules tells us what the offset of the element is. But on the other hand we can be sure what total offsets are only if we use element offset and it's outerWidth/Height.

If we decide to forget about data-offset-{edge} value then it would be smart to provide a way for modules to alter the calculations for specific element, as it was in the method that I've attached earlier.

trawekp-1’s picture

I've found out that there is a bug with patch from #29 and #45. When changing tray from vertical to horizontal "data-offset-left" was not changed to 0, so this resulted with wrong placement of tray on overlay.

jessebeach’s picture

trawekp, I'm working on an update of your patch in #46. I should have that posted today. There was some funkiness with its interaction with the Overlay that I'm smoothing out. Otherwise it looks great so far!

jessebeach’s picture

trawekp, this update to the patch addresses issues with the overlay and tableheader sticky tables. I also worked on the overlay styling a little so that it fits into a narrow space with more grace.

This is what I think we have left to do:

  1. We need some kind of cacheing or at least debouncing so we're not going into the DOM on each window resize.
  2. We need tests using testswarm.
  3. The code should be gone over and cleaned up for core: comments, docblocks, etc.

There is an example of using Testswarm to set up front end tests here: http://drupal.org/files/mobile-preview-1741498-67.patch

What do you think trawekp? Want to get this up to the line then I'll do a review? I'll try to get some other folks in here as well now that we're pretty far along with it.

jessebeach’s picture

Title: Vertical toolbar doesn't work with overlay » Generalize the measurement and tracking of displacing elements like the toolbar and provide change events to interested modules
David_Rothstein’s picture

Title: Generalize the measurement and tracking of displacing elements like the toolbar and provide change events to interested modules » Vertical toolbar doesn't work with overlay (measure/track displacing elements better + provide change events for them)

Hard to get all the relevant things in the title and make them fit...

I was going to mention that this sounds a bit like the old issue at #787940: Generic approach for position:fixed elements like Toolbar, tableHeader but I see you already added a link to this one there.

trawekp-1’s picture

Finally I have some time to continue. I've noticed that all my patches were incomplete, now I know how to do this right.

I changed some styles in core/modules/overlay/overlay-child.css. I think that box-sizing is not necessary as #overlay is a block element so as a default it will have 100% of width. As long as we don't define "width: 100%;" element will have 100% including paddings (so it's outerWidth won't exceed 100%). Correct me if I'm wrong. Because box-sizing is not supported in IE8 it's better not to use it if we don't have to.

I think that caching is not needed, we should only decrease number of Drupal.displace() calls. To achive that I removed all Drupal.displace() calls, instead of that I've added an event called "drupalOffsetChange" that displace listens to and runs debounced Drupal.displace. Also I've removed a binding to resize event as it is not necessary now. So every element is triggering "drupalOffsetChange" only when needed. Debounce guarantees 5 executions in 1s now, i'm not sure if that's the right amount.

If patch is correct then I'll move on to the remaining points.

jessebeach’s picture

trawekp, box-sizing is one of those rare CSS properties that enjoys robust support in older versions of IE. It's very handy indeed. I'd rather keep it in so that we get consistent width calculation, especially if we set min or max widths on the overlay container.

http://caniuse.com/#search=box-sizing

Ok, I agree that getting rid of the resize handler in displace.js is a good thing. I went one step farther in this patch and removed the behavior altogether:

-  Drupal.behaviors.drupalDisplace = {
-    attach: function (context, settings) {
-      // once() returns a jQuery set. It will be empty if no unprocessed
-      // elements are found. window and window.parent are equivalent unless the
-      // Drupal page is itself wrapped in an iframe.
-      $(window.parent.document.body).once('drupalDisplace', function () {
-        // If this window is itself in an iframe it must be marked as processed.
-        // Its parent window will have been processed above.
-        // When attach() is called again for the preview iframe, it will check
-        // its parent window and find it has been processed. In most cases, the
-        // following code will have no effect.
-        $(window.document.body).once('drupalDisplace');
-        findDisplacingElements();
-        // Displace attaches a drupalOffsetChange handler to the parent body
-        // the handler is wrapped with debounce 
-        // in case of offsets changed on every resize
-        $(window.parent.document.body).on('drupalOffsetChange.drupalDisplace', 
-          Drupal.debounce(displace, 200));
-        });
-    });
-  })
- };

I'd rather we put the trigger of the offset change in the Drupal.displace() interface, so I changed this code back:

-    $toolbar.trigger('drupalOffsetChange');
+    // Trigger a recalculation of viewport displacing elements.
+    Drupal.displace();

One of the purposes of the using Drupal.displace is to eliminate the need for modules to know how to calculate the displacement values and what event to trigger.

Your changes inspired me to change the passive displacement element discovery process into an active registration process. This way we don't need to go through the DOM each time we want to calculate the offsets on the off chance that another displacing element is present. Now, the toolbar registers the elements that have the ability to displace the overlay.

/**
   * Registers elements that should be used to determine viewport offset sizes.
   */
  displace.registerElements = function (elementsByEdge) {
    // Go through each edge in elementsByEdge.
    for (var edge in elementsByEdge) {
      if (elementsByEdge.hasOwnProperty(edge)) {
        // Each element or set of elements must be listed under an edge
        // property.
        if ($.inArray(edge, ['top', 'right', 'bottom', 'left']) === -1) {
          continue;
        }
        var elements = elementsByEdge[edge];
        // Wrap the elements in an array if just one element is provided.
        elements = (!'length' in elements) ? [elements] : elements;
        // Loop through each element and add it to the list of displacing
        // elements for the given edge.
        for (var i = 0, n = elements.length; i < n; i++) {
          // The elements will be stored as DOM nodes for consistency.
          var el = ('get' in elements[i]) ? elements[i].get() : elements[i];
          displacingElements[edge].push(el);
        }
        // Sort the displacing elements for the edge and remove duplicates.
        displacingElements[edge] = $.unique(displacingElements[edge]);
      }
    }
  };

Let me know what you think of this approach. I think it solves our efficiency issues.

trawekp-1’s picture

Box-sizing does not work with IE7, so my mistake.

I'm not sure why calling a method is better than triggering an event.

Do you have in mind that fluid elements would call the method on every window resize? Debounce would be a good thing in such situation.

trawekp-1’s picture

Your changes inspired me to change the passive displacement element discovery process into an active registration process. This way we don't need to go through the DOM each time we want to calculate the offsets on the off chance that another displacing element is present. Now, the toolbar registers the elements that have the ability to displace the overlay.

I was thinking of this idea for a long time. I had some difficulties with managing which element displaces which edges. I think it would be better not to store theme sepratly based on edge, but in one array. It would be good idea because toolbar for example can change from top edge to left edge displacement.

Let me know what you think of this approach. I think it solves our efficiency issues.

I think that using debounce would be a good idea. It can be still added to Drupal.displace() method easily.

As I can see you've removed a behavior. I guess you hadn't have enough time and this is something I should take care of. I like the idea o registering elements much more than the previous one. I hope we're getting close. I need some rest now but I'l try to post some code tomorrow.

jessebeach’s picture

This patch improves the offset value calculation. Also, I removed an event trigger that passed the offset change event to the document in the iframe. It's unnecessary to propagate this event down into the iframe document.

-    // Workaround because of the way jQuery events works.
-    // jQuery from the parent frame needs to be used to catch this event.
-    parent.jQuery(document).on({
-      'drupalViewportOffsetChange': function (event, offsets) {
-        // Fires an event that the child iframe can listen to.
-        $(document).trigger('drupalViewportOffsetChange', offsets);
-      }
-    });
I'm not sure why calling a method is better than triggering an event.

In displace.js, we provide the offsets as a parameter when the drupalViewportOffsetChange event is triggered:

function displace() {
  calculateOffsets();
  $(document).trigger('drupalViewportOffsetChange', offsets);
}

If we just trigger this event from toolbar.js, then we would have to calculate the offsets in toolbar.js and send those along when the event is triggered. By calling Drupal.displace(), we put that logic in a common location and toolbar no longer needs to worry about it.

Do you have in mind that fluid elements would call the method on every window resize? Debounce would be a good thing in such situation.

Right now, nothing using Drupal.displace is registering elements that need to be checked on every resize event. Toolbar monitors its elements and calls Drupal.displace when they change. For the toolbar height change, it wraps its resize handler in Drupal.debounce. We might eventually need to put a resize handler in Drupal.displace later, but at the moment, we don't need it.

jessebeach’s picture

And the patch.

As I can see you've removed a behavior. I guess you hadn't have enough time and this is something I should take care of.

I don't think we have a use case for this behavior yet.

trawekp-1’s picture

I haven't noticed that now toolbar registers it's elements so you're right. Behavior is not needed now.

If we just trigger this event from toolbar.js, then we would have to calculate the offsets in toolbar.js and send those along when the event is triggered. By calling Drupal.displace(), we put that logic in a common location and toolbar no longer needs to worry about it.

But it was a different event and Drupal.displace was called when that event was triggered. I used event because it was easier to attach a debounce. If we're not going to use debounce now then it's better to stay with method, as it is more elegant (i guess).

I've merged all elements into one array (elements are no more grouped by edge). I think that code seems simplier now. I'm still worried that we're completely not prepared for possibility that one element overlaps another. This way offsets would accumulate, but that's wrong.

Calculating offsets should be smarter. As long as there is one element per edge it's ok but there might be more (we are allowing for this) calculations might get totally wrong. I think that the only way to resolve this problem is to calculate offsets all by ourselves. Calculations would be still simple so it's worth trying.

Modules would only register elements and call Drupal.displace, nothing more. While registering there should be an edge specified that should be considered during calculations. Also we have to add possibility to re-register element (for example when an edge should change). So no more data-offset-{edge} attribute.

Do you like the idea?

jessebeach’s picture

But it was a different event and Drupal.displace was called when that event was triggered. I used event because it was easier to attach a debounce. If we're not going to use debounce now then it's better to stay with method, as it is more elegant (i guess).

Currently, the toolbar module is wrapping the setHeight method in a debounce function. The tray width (left offset) is triggered on a matchMedia change event, not screen resize, so we don't need to debounce it. I think we're set here.

// Call setHeight on screen resize. Wrap it in debounce to prevent
// setHeight from being called too frequently.
var setHeight = Drupal.debounce(Drupal.toolbar.setHeight, 200);
// Attach behavior to the window.
$(window)
  .on('resize.toolbar', setHeight);

From your comments in #57, I see an incompatibility in these two approaches you describe. It seems you took the first one in your patch, but are thinking about following the second.

I've merged all elements into one array (elements are no more grouped by edge). I think that code seems simplier now.

Calculating offsets should be smarter. As long as there is one element per edge it's ok but there might be more (we are allowing for this) calculations might get totally wrong. I think that the only way to resolve this problem is to calculate offsets all by ourselves. Calculations would be still simple so it's worth trying.

If we eliminate the edge as a key in the object of displacing elements, then it becomes impossible to calculate those offsets in Drupal.displace. I agree with your second quote. Here's my logic:

  1. Calculating viewport offsets is non-trivial for any element that isn't right up against the edge.
  2. The toolbar calculates its own offsets because of the CSS of its contents, so we need a way for an offset element to override Drupal.displace calculations.
  3. In the future, module developers might want to develop their own administration toolbars. They should have to reinvent placement and interaction with the overlay.
  4. So, we need a common way to calculate offsets for both core and contrib modules.

So, where do we go? I followed your thinking about making the offset calculation smarter. It now takes the element's offset value into account. And only the largest displacing element for an edge is used. So if multiple displacing elements exist on an edge, the smaller ones are ignored.

function calculateOffsets () {
  // Go through each edge and add up the displacements.
  for (var edge in displacingElements) {
    if (displacingElements.hasOwnProperty(edge)) {
      var displacement = 0;
      // Zero out the offset for this edge.
      offsets[edge] = 0;
      // Go through the displacing elements on each edge and calculate the
      // offset.
      for (var i = 0, n = displacingElements[edge].length; i < n; i++) {
        var $el = $(displacingElements[edge][i]);
        // If the element is not visble, do not use its dimensions.
        if (!$el.is(':visible')) {
          continue;
        }
        // If the offset data attribute contains a displacing value, use it.
        displacement = parseInt($el.attr('data-offset-' + edge), 10);
        // If the element's offset data attribute does not contain a value,
        // try to get the displacing dimension from the element directly.
        if (typeof displacement === 'undefined' || typeof displacement !== 'number' || isNaN(displacement) || displacement < 0) {
          var size = 0;
          // Get the offset of the element itself.
          var placement = $el.offset()[(edge === 'left' || edge === 'right') ? 'left' : 'top'];
          // Subtract scroll distance from placement to get the distance
          // to the edge of the viewport.
          placement = placement - window['scroll' + ((edge === 'right' || edge === 'left') ? 'X' : 'Y')];
          // Find the displacement value according to the edge.
          switch (edge) {
            // Left and top elements displace as a sum of their own offset value
            // plus their size.
            case 'top':
              size = $el.outerHeight();
              // Total displacment is the sum of the elements placement and size.
              displacement = placement + size;
              break;
            case 'left':
              size = $el.outerWidth();
              // Total displacment is the sum of the elements placement and size.
              displacement = placement + size;
              break;
            // Right and bottom elements displace according to their left and
            // top offset. Their size isn't important.
            case 'bottom':
              var docSize = document.documentElement.clientHeight;
              displacement = docSize - placement;
              break;
            case 'right':
              var docSize = document.documentElement.clientWidth;
              displacement = docSize - placement;
              break;
            default:
              displacement = 0;
          }
        }
        // If the displacement value is larger than the current value for this
        // edge, use the value.
        if (displacement >= 0 && displacement > offsets[edge]) {
          // Store the displacement value in the closure's offsets variable.
          offsets[edge] = displacement;
        }
      }
    }
  }
}

For instance, the overlay is being displaced here by two elements (orange and red) that a offset and bigger than the toolbar elements.

Screenshot of the overlay being displaced on the top and left by DOM elements that a bigger and farther from the viewport than the toolbar components

We've still got the [data-offset-{{edge}}] override for toolbar, too.

I also wrote a test for calculateOffsets method. It's in a patch file included in this post. It applies to the 8.x-1.x branch of the testswarm project. This is not ideal. I talked to some of the Drupal JS folks and I think we'll be creating a new module that will contain all the Drupal 8 front end tests. Until then, we're stuck with this patch. Once you've got the testswarm module installed (You'll need to enable the testswarm_tests module inside of the project as well), go to this path on your installation.

?testswarm-test=displace&debug=on

The displace test should run. The file for the test is drupal.displace.test.js in the testswarm_tests submodule of the project.

Work left to do

We need to fix the Drupal.overlay.eventhandlerAlterDisplacedElements method in overlay-parent.js. Somewhere in there, it's restyling the top and bottom displacing elements so that this happens:

Screenshot of the overlay. There are two elements that appear to be cut off on the right side because the overlay is setting a max-width on them that is much more narrow than the document

There are two elements that appear to be cut off on the right side because the overlay is setting a max-width on them that is much more narrow than the document. This code was originally there to prevent the iframe's scrollbar from going underneath the toolbar. It's not doing the right calculations any more.

The other issue is the Drupal.displace.removeElements method that needs work in a scenario where we're using the edge as a key to the displacing element object.

trawekp-1’s picture

If we eliminate the edge as a key in the object of displacing elements, then it becomes impossible to calculate those offsets in Drupal.displace. I agree with your second quote. Here's my logic:

Not at all. "data-offset-{edge}" tells us when to calculate the edge. Also there is no problem to store the edge with an element (but your newest patch makes me totally happy of data-offset-{edge}).

The reason why I wanted this change is simple. Take a look at toolbar module for example. There is a tray, that changes from vertical to horizontal orientation. It is possible that a module would add something similar (that changes orientation) and store both top and left displacement on one element. This way one element has to be registered for top and left edge.

We have one element doubled, also we have to tell displace.js that one of displacement equals zero. We walk into the same element twice during the calculations. Code also looks more complicated.
I just don't see a reason to force grouping elements by an edge. If we would allow calculations only for one edge then it would make sense but we don't. So according to performance my idea is better. Also my idea does not exclude any of existing logic we have discussed already.
You've said that we need a possibility to remove element for a specified edge.. nothing easier. Just remove data-offset-{edge} attribute (when element displaces more than one edge) or un-register it.

Any way I have done minor changes to your patch and implemented removeElements again. Hope this is ok. This time i did not changed any logic.

jessebeach’s picture

Not at all. "data-offset-{edge}" tells us when to calculate the edge. Also there is no problem to store the edge with an element

So what if we say that if an element has [data-offset-{edge}], use it to calculate the offset and since no value is provided, calculate it. If an element has [data-offset-{edge}="{value}"], use it to calculate the offset and use the value of the attribute to do it (skip calculations).

This would eliminate the need for registration and turn displace into a simple API for triggering a change event and calculating offsets if necessary.

I can totally back that.

trawekp-1’s picture

This would eliminate the need for registration and turn displace into a simple API for triggering a change event and calculating offsets if necessary.

I'm still using the registration. Now everything looks nice but calculateOffsets. It is written very smart but still it can scare you. This needs some clean up. I think that moving direct calculation to another function would be good. Should we do that?

I have not written behavior because I don't know how to implement element removing. While registration/unregistration works fine, removing elements registered by behavior would not be that simple (to expose as a method for modules that would be easy to understand how to use). We could be calling registerElement inside behavior, then removing elements would work the same as now and it would be simple then. I hope you have a better idea.

Once you've got the testswarm module installed (You'll need to enable the testswarm_tests module inside of the project as well), go to this path on your installation.

Is this module included in core? I can't find it there, on drupal.org too.

jessebeach’s picture

Testswarm: http://drupal.org/project/testswarm
Front-end Tests: http://drupal.org/project/fat

I'm working on responses to your questions in #61.

jessebeach’s picture

Testswarm: http://drupal.org/project/testswarm
Front-end Tests: http://drupal.org/project/fat

From #61

Now everything looks nice but calculateOffsets. It is written very smart but still it can scare you. This needs some clean up. I think that moving direct calculation to another function would be good. Should we do that?

I moved this code into a function called getRawOffset.

I really wanted to explore what this code would look like without element registration. It ends up being a lot cleaner. I also turned the displace method into one that returns the calculated offsets. Passing false bypasses recalculation of the offsets and just returns the cached values. So under this approach, a module only ever calls Drupal.displace() and/or registers for the drupalViewportOffsetChange event.

Under this approach, Toolbar lets displace calculate the width of the trays, but it explicitly sets the height of the bar.

What do you think? Displace is only being called when triggered by events like the overlay loading or the orientation of the toolbar tray changing. I'm ok with the relative inefficiency of going through the DOM to find the offsetting elements if it only happens on a trigger like resizing the screen. That isn't going to happen on small devices.

trawekp-1’s picture

What do you think? Displace is only being called when triggered by events like the overlay loading or the orientation of the toolbar tray changing. I'm ok with the relative inefficiency of going through the DOM to find the offsetting elements if it only happens on a trigger like resizing the screen. That isn't going to happen on small devices.

I think that registering elements would be better. I know that I'm boring but there might be an element calling Drupal.displace on every window resize. Fluid layouts are popular for smaller screens and smaller screens does not have powerful processors.

Please, have a look at Ubuntu Touch, I have mentioned it before but I'm not sure if you got it:
http://youtu.be/h384z7Ph0gU?t=2m14s
There are two apps running, and theirs widths can be resized. So imagine a fluid toolbar with such a device. We should be prepared for such things.

If you ask me, some kind of registering would be great. I think that behavior that searches elements with "displace" class would be just fine. A class is not a big deal. According to calling Drupal.displace() on every resize event... i guess we can hope that modules would wrap it with debounce function but we can't be sure. At least we should mention about debounce in comments of the displace method, that should be enough.

Anyway I really like the code now.

webchick’s picture

Issue tags: +JavaScript

Tagging as JavaScript so it gets on peoples' radars.

Great collaboration, you two! :D Keep it up!

jessebeach’s picture

Status: Needs work » Needs review

Setting to needs review to get some other folks to chime in.

This isn't a "needs review" for RTBC. I know there are lots of comment issues and cleanup, so don't go nuts with dreditor. This is a "needs review" for the approach. We're debating two approaches.

Displacing elements are marked with a data-offset-{edge} attribute and auto-discovered when an offset calculation is triggered

Pro
It's easy for module developers to mark an element as a displacing element.

Con
If displace is triggered often (like on window.onresize), this could be very inefficient.

Displacing elements are registered with displace.registerElements()

Pro
The DOM is scanned for displacing elements on each trigger of displace; this is better for performance.

Con
It is slightly more complicated for module developers to mark elements as displacing.

I like the cleanliness of the code and simpler DX in option 1, but option 2 seems like it will have a better performance profile. Looking for input from others.

Berdir’s picture

I just cancelled a ton of tests on the testbot.

If you don't want tests to be run, please use the do-not-test.patch suffix. Uploading a patch and not setting the issue to needs review means that by the time someone does that, all patches are sent to the testbot, which means you have to wait longer for the test results of the patch you actually want to be tested :)

nod_’s picture

displace.js

I like the patch, this is making me uncomfortable but I guess it works for now. Might end up as a perf hit too: if (!$el.is(':visible'))

Why if (typeof displacement !== 'number' || isNaN(displacement)) and not just if (isNaN(displacement)) ?

Not getting

          // If the displacement value is larger than the current value for this
          // edge, use the displacement value.
          offset[edge] = Math.max(offset[edge], displacement);

Why use the biggest number? an offset can't ever shrink ? shouldn't there be a += somewhere too?

calculateOffsets Will need to be profiled and optimized once the rest is RTBC-able.

overlay-parent.js

this.viewportOffsets = Drupal.displace(); There is either a naming problem or there is a method missing. It doesn't make much sense that Drupal.displace would return viewport offsets.

Also if you're using .on, change all the other .bind next to it or use .bind, consistency ++

.add(), really? one call was enough :þ

tableheader.js

$(window).trigger('scroll.TableHeader'); the code is already executed on a scroll event, shouldn't need to trigger another one.

toolbar.js

The way Drupal.displace(); is used ought to be an event really.

Overall

So we can't have it compute the offset for left, right, top, bottom independently? seems like overkill. Tableheader would only use offset top but needs to go through the 3 others. See #1870006: HTML5 validation with table sticky header is misaligned over the toolbar for what I mean.

I'm mixed about getRawOffset, it means the person will add a data-offset-xxx with rubish values in it? I ought to be the responsibility of the person adding this value to make sure it's the one that should be used. See my patch above, makes things simpler.

data-offset-{edge} More discoverable than the alternative. The pro for registerElement is really negligible here, qSA is really fast and with both methods you'll need to query the DOM for the offset value anyway so it's pretty much the same anyway, the first one has the advantage of better DX.

@jesse: have a look at #1440628: [Meta] javascript toolbar/tableheader with url fragment mess too we'll have enough wierd cases to support, no need to make this one overly complex or friendly.

Sorry, not well set up for an interdiff here.

jessebeach’s picture

FileSize
30.22 KB

Not getting

// If the displacement value is larger than the current value for this
// edge, use the displacement value.
offset[edge] = Math.max(offset[edge], displacement);

Why use the biggest number? an offset can't ever shrink ? shouldn't there be a += somewhere too?
calculateOffsets Will need to be profiled and optimized once the rest is RTBC-able.

The thinking here is this. If we have two elements on the left edge that both offset the edge of the viewport, we really only want to consider the largest/most-offset one rather than add their widths.

Mockup of a browser window. A dialog is open. Two elements to the left of the dialog are capable displacing the dialog left edge. The element that is farthest from the left edge pushes the dialog, not the sum of the widths of the elements.

Why if (typeof displacement !== 'number' || isNaN(displacement)) and not just if (isNaN(displacement)) ?

I'm being overly cautious I guess! I'll reduce it to just isNaN and add some tests for funky values of data-offset-{edge}.

Also if you're using .on, change all the other .bind next to it or use .bind, consistency ++

I'll just use bind for now in files that have it. I don't want to go refactoring stuff like that in this patch.

The way Drupal.displace(); is used ought to be an event really.

I struggle with this one. Currently, calling Drupal.displace() does 2 things.

  1. Recalculates the edge offsets
  2. Triggers the drupalViewportOffsetChange event.

If a module like Toolbar were to trigger drupalViewportOffsetChange instead of invoke Drupal.displace(), the recalculation step would be skipped. I'm having trouble understanding how we'd get the offset information updated if modules were responsible for triggering the event and not the utitlity.

I'm mixed about getRawOffset, it means the person will add a data-offset-xxx with rubish values in it? I ought to be the responsibility of the person adding this value to make sure it's the one that should be used. See my patch above, makes things simpler.

Yes, a module could provide a value in data-offset-{edge} and that value would be used. The purpose of the getRawOffset</code. method is to provide a common means of calculating an offset in situations where the module doesn't need to. For instance, the toolbar tray left offset is just a simple box so it adds the <code>data-offset-left attribute and lets Drupal.displace calculate the offset. The toolbar bar is a different story because of its CSS. It need to calulate the top offset and provide that value to Drupal.displace.

The calculation of an offset for a position fixed element when the screen is scrolled and the element is not flush against the viewport edge is not trivial. Because of this, I thought it would be useful to provide this calculation as a utility of Drupal.displace where we can unit test it, rather than have modules figure it out individually.

jessebeach’s picture

Finally, I've gotten through the problems with Drupal.overlay.eventhandlerAlterDisplacedElements. This code was complex and I believe I've cut it down significantly without losing any behavior.

So, this is the patch that we start considering for commit. These are the improvements it introduces

  1. Toolbar and Overlay now interact well. This was the primary goal of this issue. When a vertical tray is active, the Overlay responds by resizing to fit in the space left to the right/left of the tray.
  2. Previously, the Overlay module and tableheader.js had independently computed their viewport offset values. Now, they leverage the new Drupal.displace utility. See also #787940: Generic approach for position:fixed elements like Toolbar, tableHeader, which this issue should resolve.
  3. The method Drupal.overlay.eventhandlerAlterDisplacedElements now has 75% less code and is much more readable. And we rid ourselves of the last vestige of $.browser in Drupal core module code!
  4. Overlay placement and sizing in RTL documents across various platforms/browser versions was not stellar in D7. This patch at least keeps parity with D7 or improves it slightly. Now with cleaner code and better viewport offset displacement, we can make further improvements. I'll put some screenshots at the end of this issue.

Changes of note in this patch

I put a window resize handler in Drupal.displace() in a behavior (and removed the resize handler from Toolbar). The reason I did this is because Toolbar and Overlay both need to respond to altered viewport displacing elements on resizes. I'd rather not have two resize handlers on window when we could just have one in a common place.

/**
 * Registers a resize hanlder on the window.
 */
Drupal.behaviors.drupalDisplace = {
  attach: function (context, settings) {
    // Do not process the window of the overlay.
    if (parent.Drupal.overlay && parent.Drupal.overlay.iframeWindow === window) {
      return;
    }
    // Mark this behavior as processed on the first pass.
    if (this.displaceProcessed) {
      return;
    }
    this.displaceProcessed = true;

    $(window).on('resize.drupalDisplace', Drupal.debounce(function (event) {
      displace(true);
    }, 200));
  }
};

Drupal.displace() calculates the offsets, broadcasts the updated values through the drupalViewportOffsetChange event and returns the offset values object to the calling function. The broadcast can be skipped by passing false to the method.

/**
 * Informs listeners of the current offset dimensions.
 *
 * @param Boolean broadcast
 *   - When true or undefined, causes the recalculated offsets values to be
 *   broadcast to listeners.
 */
function displace (broadcast) {
  offsets = calculateOffsets();
  if (typeof broadcast === 'undefined' || broadcast) {
    $(document).trigger('drupalViewportOffsetChange', offsets);
  }
  return offsets;
}

Also worth noting is that these changes feed directly into #1860434: Refactor the Toolbar JavaScript to use Backbone; fix several poorly functioning behaviors. Once we get this committed, I'll work these changes into the updated Toolbar JavaScript for even more improvements and less code.

What we need now

More permutation testing

I've tried to test as many OS/browser combos as possible (in both LTR and RTL), comparing D7 to D8, to make sure that there are no regressions. In many cases, the broken D7 behavior continues without improvement in D8 because the machinations of overlay-parent.js and the styling of the overlay are just not going to result in a harmonious arrangement of elements in all permutations. We can't reliably know the width and placement of a scrollbar in all browsers on all platforms in all language directions. I spent more than 8 hours breaking my brain trying to prove this statement wrong and I couldn't solve it (not to say it's unsolvable). The better approach is to continue making design improvements to the Overlay presentation, specifically those being discussed in #1889150: Simplify overlay look, *only use for contextual operations* and http://groups.drupal.org/node/283223.

Code review

I'd love to get this issue committed this week so we can focus on further improvements that will build on this code.

Screenshots

Screenshot of a D7 site in IE9 on Windows 7 in an RTL language

Screenshot of a D8 site in IE9 on Windows 7 in an RTL language

What's up with the borked styling on the close tab you ask? Well, this is one of those great IE issues that you dream about. Watch this video to see how IE mixes up right and left for border radius in RTL languages: http://screencast.com/t/xXrZGYPNrr. There is no way to fix this except through IE-targeted CSS which we aren't doing in D8. I hope this resolves through styling changes that a proposed for Overlay that eliminate this hanging chad element. If not, we'll have to reconsider this again before release, but my prediction is that it'll resolve itself in due time.

Screenshot of a D7 site in Chrome on Windows 7 in an RTL language

Screenshot of a D8 site in Chrome on Windows 7 in an RTL language

rteijeiro’s picture

Status: Needs review » Needs work

Hi Jesse,

I have applied the patch and now the toolbar is well placed beside the overlay but I have noticed a little horizontal scroll in the overlay (also with the toolbar collapsed). This issue appears in Chrome, Firefox and Safari with Drupal 8. Surprising it doesn't appear in Opera :)

Should I test the patch also in Drupal 7?

jessebeach’s picture

This patch won't apply in Drupal 7.

I think the scrollbar is caused by the hanging close tab. The redesign of the overlay should fix this.

#1953374: Implement Seven style guide for core overlay

rteijeiro’s picture

Status: Needs work » Reviewed & tested by the community

Ok, so if the horizontal scroll would be fixed in #1953374: Implement Seven style guide for core overlay the I guess this should be considered as RTBC.

If you don't agree with me or maybe it's needed to be reviewed by someone more experienced than me, just fix the status ;)

Thanks Jesse :)

nod_’s picture

Status: Reviewed & tested by the community » Needs work

Slow down :)

Patch incoming.

nod_’s picture

Status: Needs work » Needs review
FileSize
32.75 KB

I put offsetTop in tableheader because there was no other object with this kind of info, now that we have Drupal.displace I used that.

Exposed Drupal.displace.offsets and Drupal.displace.calculateOffset so that other modules can use it. Removed some unneeded jQuery in calculateOffset when there is a value to use.

in overlay-child, removed the resize event, don't know why you added this one on top of the drupalViewportOffsetChange

In overlay-parent same as tableheader, used Drupal.displace.offsets to handle the values. added a couple of ";" and a useless var in the repaint because jshint was complaining.

nod_’s picture

FileSize
10.81 KB

wow, interdiff fail. the one above is not mine.

jessebeach’s picture

Status: Needs review » Reviewed & tested by the community

The changes in #75 look great. Thanks for putting that together nod_! I think we're ready to get this in.

One thing you changed in the patch from #70 is the comment format for functions. Here's an example.

-   * @param boolean broadcast
+   * @param {boolean} broadcast

According to http://drupal.org/node/172169, we should be using doxygen format for type descriptors of function params and returns. The doxygen format does not include {} around the type descriptor. I've seen both in our codebase. Personally I'd rather go without the brackets, but I don't have a good sense of what the docs parsing requires. Do you know if the brackets are necessary? If so, we'll want to update the JS documentation docs.

attiks’s picture

Regarding the docs, jsdoc3 allows both "boolean" and "{boolean}", but - me personally - I would prefer "boolean", less typing and easier to read.

webchick’s picture

Title: Vertical toolbar doesn't work with overlay (measure/track displacing elements better + provide change events for them) » Change notice: Measure/track displacing elements better + provide change events for them (fix overlay + toolbar)
Category: bug » task
Priority: Major » Critical
Status: Reviewed & tested by the community » Active
Issue tags: +Needs change record

Committed and pushed to 8.x. AWESOME! :D

Probably need a change notice about this new displace library.

jessebeach’s picture

I'll put together the change notice and post that today.

xjm’s picture

Let's also file that followup to strip the {} from the docblock datatypes.

xjm’s picture

Issue tags: +Needs followup

Instituting a new issue tag since I've noticed a lot of issues lately where the followups don't get filed. :)

star-szr’s picture

+++ b/core/misc/displace.jsundefined
@@ -0,0 +1,179 @@
+   * @param {jQuery} $el
+   *   The jQuery element whose dimensions and placement will be measured.
+   *
+   * @param {string} edge
+   *   The name of the edge of the viewport that the element is associated
+   *   with.

What about the blank lines in between each @param? Should those be left for any reason?

klonos’s picture

xjm’s picture

@klonos, you don't need to post a comment to track the issue. Just click the green "Follow" button upstairs in the issue summary. :)

@Cottser, agreed, we can clean that up in a followup too.

star-szr’s picture

Should the followup for removing {} and extra lines from @params ask for a patch for all core JS or just for this library?

klonos’s picture

@xjm: Yes, I know. I do this in order to remember the issue I originally came from. This helps for example when issues that were favored over their duplicates are wontfixed. This helps me get back to the issue marked as duplicate of the wontfixed issue and re-activate it ;)

jessebeach’s picture

Title: Change notice: Measure/track displacing elements better + provide change events for them (fix overlay + toolbar) » Measure/track displacing elements better + provide change events for them (fix overlay + toolbar)
Status: Active » Closed (fixed)

Change notice was posted [#1956804].

Setting to fixed.

star-szr’s picture

Tags updated, here is a follow-up for the docs touch-ups:

#1958246: Update JS documentation to follow Doxygen format (remove curly braces)

Wim Leers’s picture

Issue tags: -sprint

.

damontgomery’s picture

Status: Closed (fixed) » Active
FileSize
45.68 KB

I'm having a similar issue here. Please see my screenshots. This is on Chrome for Mac. This does NOT happen in Firefox on Mac. This is a fresh install of d8-dev from today, Jun 4, 2013. If this was the wrong issue, sorry, I couldn't find another that seemed more related.

jessebeach’s picture

Hi pandaeskimo, do you see any other JavaScript errors on the page? Could you post the top style value of the overlay from the Inspector Tools?

rteijeiro’s picture

@pandaeskimo this issue was closed and fix commited so I guess it's better to open a new issue for that.

rteijeiro’s picture

Tested in Chrome for Mac Version 27.0.1453.93 and can't reproduce. Something to take into consideration or maybe I should close the issue again?

damontgomery’s picture

Status: Active » Closed (fixed)

Sorry about this, I tested it a bunch more and it's possible it was a caching issue that has been resolved locally. Reverting the issue to closed (fixed).

damontgomery’s picture

Issue summary: View changes

Add followup to issue summary