Wednesday Apr 11, 2012

More Animation - Self Dismissing Dialogs

In my earlier articles on animation, I discussed various slide, grow and  flip transitions for items and containers.  In this article I want to discuss a fade animation and specifically the use of fades and auto-dismissal for informational dialogs.  If you use a Mac, you may be familiar with Growl as a notification system, and the nice way that messages that are informational just fade out after a few seconds. So in this blog entry I wanted to discuss how we could make an ADF popup behave in the same way. This can be an effective way of communicating information to the user without "getting in the way" with modal alerts. This of course, has been done before, but everything I've seen previously requires something like JQuery to be in the mix when we don't really need it to be. 

The solution I've put together is nice and generic and will work with either <af:panelWindow> or <af:dialog> as a the child of the popup. In terms of usage it's pretty simple to use we  just need to ensure that the popup itself has clientComponent is set to true and includes the animation JavaScript (animateFadingPopup) on a popupOpened event:

<af:popup id="pop1" clientComponent="true">
  <af:panelWindow title="A Fading Message...">
   ...
 </af:panelWindow>
  <af:clientListener method="animateFadingPopup" type="popupOpened"/>
</af:popup> 

 The popup can be invoked in the normal way using showPopupBehavior or JavaScript, no special code is required there.

As a further twist you can include an additional clientAttribute called preFadeDelay to define a delay before the fade itself starts (the default is 5 seconds) . To set the delay to just 2 seconds for example:

<af:popup ...>
  ...
  <af:clientAttribute name="preFadeDelay" value="2"/>
  <af:clientListener method="animateFadingPopup" type="popupOpened"/> 
</af:popup>

The Animation Styles 

As before, we have a couple of CSS Styles which define the animation, I've put these into the skin in my case, and, as in the other articles, I've only defined the transitions for WebKit browsers (Chrome, Safari) at the moment. In this case, the fade is timed at 5 seconds in duration.

.popupFadeReset {
  opacity: 1;
}

.popupFadeAnimate {
  opacity: 0;
  -webkit-transition: opacity 5s ease-in-out;
}

As you can see here, we are achieving the fade by simply setting the CSS opacity property.

The JavaScript

The final part of the puzzle is, of course, the JavaScript, there are four functions, these are generic (apart from the Style names which, if you've changed above, you'll need to reflect here):

  1. The initial function invoked from the popupOpened event,  animateFadingPopup which starts a timer and provides the initial delay before we start to fade the popup.
  2. The function that applies the fade animation to the popup - initiatePopupFade.
  3. The callback function - closeFadedPopup used to reset the style class and correctly hide the popup so that it can be invoked again and again.  
  4. A utility function - findFadeContainer, which is responsible for locating the correct child component of the popup to actually apply the style to.

Function - animateFadingPopup

This function, as stated is the one hooked up to the popupOpened event via a clientListener. Because of when the code is called it does not actually matter how you launch the popup, or if the popup is re-used from multiple places. All usages will get the fade behavior.

/**
 * Client listener which will kick off the animation to fade the dialog and register
 * a callback to correctly reset the popup once the animation is complete
 * @param event
 */
function animateFadingPopup(event) {
  var fadePopup = event.getSource();
  var fadeCandidate = false;
  //Ensure that the popup is initially Opaque
  //This handles the situation where the user has dismissed
  //the popup whilst it was in the process of fading
  var fadeContainer = findFadeContainer(fadePopup);
  if (fadeContainer != null) {
    fadeCandidate = true;
    fadeContainer.setStyleClass("popupFadeReset");
  }
  //Only continue if we can actually fade this popup
  if (fadeCandidate) {
    //See if a delay has been specified
    var waitTimeSeconds = event.getSource().getProperty('preFadeDelay');
    //Default to 5 seconds if not supplied
    if (waitTimeSeconds == undefined) {
      waitTimeSeconds = 5;
    }
    // Now call the fade after the specified time
    var fadeFunction = function () {
      initiatePopupFade(fadePopup);
    };
    var fadeDelayTimer = setTimeout(fadeFunction, (waitTimeSeconds * 1000));
  }
}

The things to note about this function is the initial check that we have to do to ensure that the container is currently visible and reset it's style to ensure that it is.  This is to handle the situation where the popup has begun the fade, and yet the user has still explicitly dismissed the popup before it's complete and in doing so has prevented the callback function (described later) from executing. In this particular situation the initial display of the dialog will be (apparently) missing it's normal animation but at least it becomes visible to the user (and most users will probably not notice this difference in any case).

You'll notice that the style that we apply to reset the  opacity - popupFadeReset, is not applied to the popup component itself but rather the dialog or panelWindow within it. More about that in the description of the next function findFadeContainer().

Finally, assuming that we have a suitable candidate for fading, a JavaScript  timer is started using the specified preFadeDelay wait time (or 5 seconds if that was not supplied). When this timer expires then the main animation styleclass will be applied using the initiatePopupFade() function

Function - findFadeContainer

As a component, the <af:popup> does not support styleClass attribute, so we can't apply the animation style directly.  Instead we have to look for the container within the popup which defines the window object that can have a style attached.  This is achieved by the following code:

/**
 * The thing we actually fade will be the only child
 * of the popup assuming that this is a dialog or window
 * @param popup
 * @return the component, or null if this is not valid for fading
 */
function findFadeContainer(popup) {
  var children = popup.getDescendantComponents();
  var fadeContainer = children[0];
  if (fadeContainer != undefined) {
    var compType = fadeContainer.getComponentType();
    if (compType == "oracle.adf.RichPanelWindow" 
     || compType == "oracle.adf.RichDialog") {
      return fadeContainer;
    }
  }
  return null;
}

 So what we do here is to grab the first child component of the popup and check its type. Here I decided to limit the fade behaviour to only <af:dialog> and <af:panelWindow>. This was deliberate.  If  we apply the fade to say an <af:noteWindow> you would see the text inside the balloon fade, but the balloon itself would hang around until the fade animation was over and then hide.  It would of course be possible to make the code smarter to walk up the DOM tree to find the correct <div> to apply the style to in order to hide the whole balloon, however, that means that this JavaScript would then need to have knowledge of the generated DOM structure, something which may change from release to release, and certainly something to avoid. So, all in all, I think that this is an OK restriction and frankly it's windows and dialogs that I wanted to fade anyway, not balloons and menus. You could of course extend this technique and handle the other types should you really want to.

One thing to note here is the selection of the first (children[0]) child of the popup. It does not matter if there are non-visible children such as clientListener before the <af:dialog> or <af:panelWindow> within the popup, they are not included in this array, so picking the first element in this way seems to be fine, no matter what the underlying ordering is within the JSF source.  If you wanted a super-robust version of the code you might want to iterate through the children array of the popup to check for the right type, again it's up to you. 

Function -  initiatePopupFade

 On to the actual fading. This is actually very simple and at it's heart, just the application of the popupFadeAnimate style to the correct component and then registering a callback to execute once the fade is done.

/**
 * Function which will kick off the animation to fade the dialog and register
 * a callback to correctly reset the popup once the animation is complete
 * @param popup the popup we are animating
 */
function initiatePopupFade(popup) {
  //Only continue if the popup has not already been dismissed 
  if (popup.isPopupVisible()) {
    //The skin styles that define the animation 
    var fadeoutAnimationStyle = "popupFadeAnimate";
    var fadeAnimationResetStyle = "popupFadeReset";

    var fadeContainer = findFadeContainer(popup);
    if (fadeContainer != null) {
      var fadeContainerReal = AdfAgent.AGENT.getElementById(fadeContainer.getClientId());
      //Define the callback this will correctly reset the popup once it's disappeared
      var fadeCallbackFunction = function (event) {
        closeFadedPopup(popup, fadeContainer, fadeAnimationResetStyle);
        event.target.removeEventListener("webkitTransitionEnd", fadeCallbackFunction);
      };
      //Initiate the fade
      fadeContainer.setStyleClass(fadeoutAnimationStyle);
      //Register the callback to execute once fade is done
      fadeContainerReal.addEventListener("webkitTransitionEnd", fadeCallbackFunction, false);
    }
  }
}

I've added some extra checks here though. First of all we only start the whole process if the popup is still visible. It may be that the user has closed the popup before the delay timer has finished so there is no need to start animating in that case. Again we use the findFadeContainer() function to locate the correct component to apply the style to, and additionally we grab the DOM id that represents that container.  This physical ID is required for the registration of the callback function. The closeFadedPopup() call is then registered on the callback so as to correctly close the now transparent (but still there) popup.

Function -  closeFadedPopup

The final function just cleans things up:

/**
 * Callback function to correctly cancel and reset the style in the popup
 * @param popup id of the popup so we can close it properly
 * @param contatiner the window / dialog within the popup to actually style
 * @param resetStyle the syle that sets the opacity back to solid
 */
function closeFadedPopup(popup, container, resetStyle) {
  container.setStyleClass(resetStyle);
  popup.cancel();
} 

First of all we reset the style to make the popup contents opaque again and then we cancel the popup.  This will ensure that any of your user code that is waiting for a popup cancelled event will actually get the event, additionally if you have done this as a modal window / dialog it will ensure that the glasspane is dismissed and you can interact with the UI again. 

What's Next?

There are several ways in which this technique could be used, I've been working on a popup here, but you could apply the same approach to in-line messages. As this code (in the popup case) is generic it will make s pretty nice declarative component and maybe, if I get time, I'll look at constructing a formal Growl component using a combination of this technique, and active data push. Also, I'm sure the above code can be improved a little too.  Specifically things like registering a popup cancelled listener to handle the style reset so that we don't loose the subtle animation that takes place when the popup is opened in that situation where the user has closed the in-fade dialog.

About

Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!

Search

Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today