Showing posts with label popup. Show all posts
Showing posts with label popup. Show all posts

28 Sept 2015

Show an Image in a Popup Window

There is a very common use-case when we have a link with an image on a page and we need to show a full-size version of the image in a popup window by clicking on the link. In this post I am going to show a few implementations of this use-case in ADF application.

Inline Popup
The first and the most obvious solution is to use af:popup and af:showPopupBehavior tags. Something like this:

<af:link text="Inline Popup" id="l1" icon="/images/turtle.jpg"
         iconPosition="top" styleClass="ImageLink">
  <af:showPopupBehavior popupId="p1"/>
</af:link>

<af:popup animate="false" contentDelivery="lazyUncached"
          childCreation="deferred" id="p1">
  <af:panelWindow resize="on" stretchChildren="first" id="pw1" title="Turtle">
    <af:image id="dc_i1" source="/images/turtle.jpg"/>
  </af:panelWindow>
</af:popup>

In this case an inline popup is going to be shown whenever the link is clicked:


So, it is pretty easy to implement and it works quite fast and nice.
What if we don't want to use an inline popup and we prefer to show the image in a separate browser window?

Separate browser tab
Let's assume the requirement is to show the image in a separate browser tab.
Actually, it is very easy to implement. We're going to use destination and targetFrame attributes of af:link component:

<af:link text="Destination" id="l2" icon="/images/turtle.jpg"
         iconPosition="top" styleClass="ImageLink"
         destination="/images/turtle.jpg" targetFrame="_blank"/>

So, whenever the link is clicked a new tab should be opened with a full-size image inside it. Actually, the "_blank" targetFrame forces the browser to open a new tab.


Separate browser window
If we want to force a browser to show the image inside a new window, we're going to use either of the following techniques:

ADF dialog: action
This is a pure ADF technique and it is based on ability to show a page in a new browser window while navigating to it. There are two pages in our page flow:

The ImageView contains an image:
  <af:form id="f1">
    <af:image source="/images/turtle.jpg" id="i1"/>
  </af:form>

And the MainView contains a link:
<af:link text="Dialog action" id="l3" icon="/images/turtle.jpg"
         iconPosition="top"
         action="dialog:showImage"
         useWindow="true" windowHeight="400" windowWidth="400"/>

Pay attention to the useWindow attribute and to the action name which starts with "dialog:" prefix. These two things force the browser to show ImageView page in a new window. The size of this new window is defined with windowHeight and windowWidth attributes.
This approach works fine, but I wouldn't say that this solution is the best one in terms of performance. Whenever the framework is processing such "dialog:" navigation it seams that a spaceship is starting  on Cape Canaveral. It may take a second or two to open this new tab.

Java Script Approach
This lightweight solution uses a java script to open a new browser window:

function showImage(event)
{
  var link = event.getSource();
  popupWindow = window.open(link.getProperty("imageSrc"),
                            'Turtle', 'height=400,width=400');
}

There is a corresponding clientListener defined for the link:
<af:link text="Java Script" id="l4" icon="/images/turtle.jpg"
         iconPosition="top" styleClass="ImageLink">
  <af:clientListener method="showImage" type="action"/>
  <af:clientAttribute value="#{ImageBean.endpointURL}/images/turtle.jpg"
                      name="imageSrc"/>
</af:link>

In order to pass correct image URI to the Java Script function we are using a managed bean method getEndpointURL:

public String getEndpointURL()
{
  FacesContext facesContext = FacesContext.getCurrentInstance();
  HttpServletRequest request =
    (HttpServletRequest) facesContext.getExternalContext().getRequest();
  String url = request.getRequestURL().toString();
  StringBuffer newUrlBuffer = new StringBuffer();
  newUrlBuffer.append(url.substring(0, url.lastIndexOf("faces/")));
  return newUrlBuffer.toString();
}

Basically this method returns URL to the application endpoint.

The sample application for this post can be downloaded here. It requires JDeveloper 12.1.3.

That's it!









31 Jul 2014

Popup, Dialog and Input Components

In this post I would like to focus on a very common use case when we have af:popup containing af:dialog with input components inside. There are a couple of pitfalls that we need to watch out for when implementing this use case.
Let's consider a simple example:

<af:popup id="p1" contentDelivery="lazyUncached">
          
  <af:dialog id="d2" title="Dialog" >
     <af:inputText value="#{TheBean.firstName}" label="First Name" id="it1"/>
     <af:inputText value="#{TheBean.lastName}" label="Last Name" id="it2"/>
  </af:dialog>
  
</af:popup>  

The most interesting thing here is the popup's property contentDelivery which is set to lazyUncached. This prevents the popup from caching the submitted input values and forces it to get the values from the model on each request instead of using values from the cache.

Let's make the example a bit more complicated. In the lastName's  setter we are going to throw an exception:

public void setLastName(String lastName) throws Exception {        
    this.lastName = lastName;        
    throw new Exception("This last name is bad");
}

So, obviously if we try to submit the dialog we'll get the following:

The input values can not be submitted to the model and they are going to be stored in the local values of the input components. These local values are not going to be cleaned up even if we press the Cancel button and these values will be used during the subsequence request. In order to prevent this behavior we have to set resetEditableValues property of the popup to whenCanceled. Like this:

<af:popup id="p1" contentDelivery="lazyUncached"
                  resetEditableValues="whenCanceled">

  <af:dialog id="d2" title="Dialog" >
     <af:inputText value="#{TheBean.firstName}" label="First Name" id="it1"/>
     <af:inputText value="#{TheBean.lastName}" label="Last Name" id="it2"/>
  </af:dialog>  
  
</af:popup>  

Let's consider an example of af:dialog with custom buttons:
<af:popup id="p1" contentDelivery="lazyUncached"
                  resetEditableValues="whenCanceled"
                  binding="#{TheBean.popup}">

  <af:dialog id="d2" title="Dialog" type="none">
     <af:inputText value="#{TheBean.firstName}" label="First Name" id="it1"/>
     <af:inputText value="#{TheBean.lastName}" label="Last Name" id="it2"/>
     <f:facet name="buttonBar">
        <af:panelGroupLayout layout="horizontal" id="pgl1">
          <af:button text="Ok" id="b2" 
                     actionListener="#{TheBean.buttonActionListener}"/>
          <af:button text="Cancel" id="b3" immediate="true"
                     actionListener="#{TheBean.buttonActionListener}"/>
        </af:panelGroupLayout>  
     </f:facet>

  </af:dialog>  
  
</af:popup>  


So, there are two custom buttons "Ok" and "Cancel" with the following actionListener:

public void buttonActionListener(ActionEvent actionEvent) {
    getPopup().hide();
}

The resetEditableValues doesn't work in this case and local values of the input components won't be cleaned up when pressing the Cancel button. There are a couple of options to fix this issue.
The first one is to add af:resetListener to the Cancel button:

          <af:button text="Cancel" id="b3" immediate="true"
                     actionListener="#{TheBean.buttonActionListener}">
               <af:resetListener type="action"/>
          </af:button>           

The second option is to cancel the popup instead of just hiding it in the Cancel button action listener:

  <af:button text="Ok" id="b2" 
             actionListener="#{TheBean.buttonActionListener}"/>
  <af:button text="Cancel" id="b3" immediate="true"
             actionListener="#{TheBean.cancelButtonActionListener}"/>


public void cancelButtonActionListener(ActionEvent actionEvent) {
   getPopup().cancel();
}
 
That's it!

8 May 2011

InputText with suggestion on demand

Introduction 
ADF Faces provide us by quite handy tag af:autoSuggestBehavior. It could be used together with some input control in order to implement very common use-case when  a user typing some text is suggested by a drop-down list with some values to be selected and displayed in the input field. The following screenshot shows an example of such behavior:

The jspx code for this example is very simple:

        <af:inputText label="Currency" id="i21">
          <af:autoSuggestBehavior 
              suggestedItems="#{InputSuggestBean.onCurrencySuggest}"/>
        </af:inputText>

The af:autoSuggestBehavior tag needs to be bounded to a backing bean method returning a list of items to be suggested. The method has only one String argument containing submitted user's value. Depending on this value you can implement your own logic for the returning list like filtering, sorting, etc. For example, the following backing bean method returns currency codes that match user's input string:

    private final static String[] ccys = {"USD", "EUR", "UAH", "CAD"};

    public List onCurrencySuggest(String inputValue) {
        ArrayList<SelectItem> suggestItems = new ArrayList<SelectItem>();
        for (String s: ccys) {
            if (s.startsWith(inputValue)) 
                suggestItems.add(new SelectItem(s));
        }
        return suggestItems;
    }

Of course, you can implement your backing bean method getting suggested items from ADF BC layer in order to retrieve them from database or from other data sources. You can find detailed example of this feature in the ADF Code Corner Article posted by Frank Nimphius.

Suggestion on demand
But there is another common use-case when users don't need any auto-suggestion for their input, and they want to be suggested on demand only. For example, by pressing "Ctrl-H", a user is provided by previously submitted values for this input field. Something like a history of values.
In this post I'm going to show two different implementations of this use-case. The first one is based on modified af:autoSuggestBehavior tag and the second implementation is built using usual af:popup tag.

Modified af:autoSuggestBehavior
  The jspx definition of inputText looks as usual:

        <af:inputText label="Modified input" id="it1">        
          <af:autoSuggestBehavior 
            suggestedItems="#{InputSuggestBean.onSuggest}"/>
        </af:inputText>

And the backing bean method looks like this:

    public List onSuggest(String string) {
        ArrayList<SelectItem> selectItems = new ArrayList<SelectItem>();
      
        //Let's assume these are history values:
        selectItems.add(new SelectItem("One"));
        selectItems.add(new SelectItem("Two"));        

        return selectItems;
    }

af:autoSuggestBehavior tag  renders some JavaScript object AdfAutoSuggestBehavior responsible for auto-suggestion functionality. It adds a number of event listeners to the inputText for different event types like 
onKeyUp, onBlur, onFocus, etc.

In order to prevent default behavior of af:autoSuggestBehavior tag I have to override its onKeyUp listener and do some JavaScript coding:

<af:document id="d1">      
 <af:resource type="javascript">
  var initialized = false; //Initialization flag
  
  initMySuggestBehavior = function () {
      if (!initialized) { 
          
         //Saving default _fireKeyUp function
         AdfAutoSuggestBehavior.prototype._fireKeyUpEx = 
            AdfAutoSuggestBehavior.prototype._fireKeyUp;

         //Writing proxy for _fireKeyUp function
         AdfAutoSuggestBehavior.prototype._fireKeyUp = function (e) {
           //Getting the event's source and its content                
           var input = e.getSource();
           if (input instanceof AdfUIEditableValue) 
             var inputContent = AdfDhtmlEditableValuePeer.GetContentNode(input);
             //If user has pressed ctrl H then fire suggestion
           if ("ctrl H" == e.getKeyStroke().toMarshalledString()) {
              AdfAutoSuggestBehavior.prototype._autoSuggest(input);
              //Just for better visualization
              if (inputContent.value.length == 0) inputContent.value = "...";
              }

           //This will prevent default suggestion firing for every input character
           if (inputContent) this._nodeLength = inputContent.value.length;
          
           //Calling default event listener we saved before
           AdfAutoSuggestBehavior.prototype._fireKeyUpEx(e);
        };
             
        //Looking for our inputText
        var inp = AdfPage.PAGE.findComponentByAbsoluteId("it1");
         
        /* At this moment AdfAutoSuggestBehavior has already initialized
         * and added his default listeners to the inputText.
         * We need to clear them and reinitialize with our overridden 
         * _fireKeyUp function.
         */  
        inp.setProperty("clientListeners", null);
        AdfAutoSuggestBehavior.prototype.initialize(inp);
         
        /* Adding a simple listener for onKeyDown event in order to 
         * prevent default browser behavior for Ctrl-H            
         */
        inp.addEventListener(AdfUIInputEvent.KEY_DOWN_EVENT_TYPE, myKeyDown, this);
                
        //We've done it.
        initialized = true;
     }
  }
//Preventing default browser behavior for Ctrl-H
 myKeyDown = function (event) {
    if ("ctrl H" == event.getKeyStroke().toMarshalledString())
       event.cancel();
  }
</af:resource>
...      

I'm going to call initMySuggestBehavior JavaScript  function in a phase listener written for the f:view tag:
<f:view beforePhase="#{InputSuggestBean.viewPhaseListener}">
    public void viewPhaseListener(PhaseEvent phaseEvent) {
        if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE) {
          FacesContext fctx = FacesContext.getCurrentInstance();
          ExtendedRenderKitService ks = 
              Service.getRenderKitService(fctx, ExtendedRenderKitService.class);
          StringBuffer script = new StringBuffer();
          script.append("window.initMySuggestBehavior()");
          ks.addScript(fctx, script.toString());            
        }

    }

The result of our modifications looks like this:


Using af:popup

The jspx page for this implementation looks like this:

    <af:popup id="ctrlHPopup" contentDelivery="lazyUncached"
              animate="false">
      <af:selectOneListbox 
               id="sol4"
               simple="true"
               autoSubmit="false"
               >
        <f:selectItems value="#{InputSuggestBean.popupSuggestion}" id="si7"/>
        <af:clientListener type="keyDown" method="enterSelection"/>
        <af:clientListener type="click" method="clickSelection"/>
      </af:selectOneListbox>
    </af:popup>

    <af:panelLabelAndMessage label="Input with popup" id="plam1">
      <af:inputText id="it2" simple="true">
       <af:clientListener method="ctrlHKeyDown" type="keyDown"/>
      </af:inputText>
    </af:panelLabelAndMessage>

I have a popup and selectOneListbox component inside it. The listbox get suggested items from the backing bean property popupSuggestion and it has client listeners to submit selected values when Enter is pressed or mouse is clicked. The inputText has a listener to fire popup with suggested items when Ctrl-H is pressed.
The backing bean has the following method:

    public List getPopupSuggestion() {
        ArrayList<SelectItem> selectItems = new ArrayList<SelectItem>();
      
        //Let's assume these are history values
        selectItems.add(new SelectItem("The first value"));
        selectItems.add(new SelectItem("The second value"));        

        return selectItems;
    }

And for sure we have some JavaScript implementation of our listeners:
        //Firing popup and preventing default browser behavior for Ctrl-H
        ctrlHKeyDown = function (event) {
            if ("ctrl H" == event.getKeyStroke().toMarshalledString()) {
              showPopup();     
              event.cancel();              
            }   
        }
        
        //Firing popup
        showPopup = function(){
         //Looking for popup component
         var popup = AdfPage.PAGE.findComponentByAbsoluteId("ctrlHPopup");
         //Define popup alignment 
         var hints = {}; 
         hints[AdfRichPopup.HINT_ALIGN_ID] = 
           AdfPage.PAGE.findComponentByAbsoluteId("it2").getClientId();
         hints[AdfRichPopup.HINT_ALIGN] = AdfRichPopup.ALIGN_AFTER_START;
         //Showing
         popup.show(hints);
        }

        //Hiding popup
        hidePopup = function(){
         var popup = AdfPage.PAGE.findComponentByAbsoluteId("ctrlHPopup");
         popup.hide();
        }    
        
        //If Enter is pressed 
        enterSelection = function(event){         
         if (AdfKeyStroke.ENTER_KEY == event.getKeyStroke().getKeyCode()) {
           setSelectedValue(event);  
         }
        }
        
        //If mouse is clicked
        clickSelection = function(event){         
           setSelectedValue(event);  
        }

        //Submit selected value and hide the popup
        setSelectedValue = function(event) {
            var items = event.getSource();
            var selectedItem = items.getSelectItems()[items.getValue()];
            var inputText = AdfPage.PAGE.findComponentByAbsoluteId("it2");
            var inputContent = AdfDhtmlEditableValuePeer.GetContentNode(inputText);
            inputContent.value = selectedItem.getLabel(); 
            hidePopup();    
        }

And the result of our work looks like this:


That's all. You can download sample application for this post.