Press "Enter" to skip to content

How to persist APEX dialog size and position

Last updated on Friday, July 24, 2020

As a developer I love how easy it is to create dialogs with APEX 5.0. But as a user I get a little annoyed that I have to move and resize them each time I open a dialog. Today I’ll show how to persist dialog position and size for the duration of a browser session.

To follow along create or edit an app with one or more modal dialog pages. You can use the Sample Dialog app as a starting point if you don’t have an app with dialogs handy. For beginning information about how to create modal dialog pages try either of these fine blog articles:

APEX 5.0 Modal dialogs have never been so easy!
Creating a Modal Dialog in APEX 5.0

First make sure your dialogs are resizable by adding resizable:true to the page Dialogs Attributes.

Dialog Attributes

Before making any changes run the app and open a dialog. Size and move the dialog and close it. Then open it again and notice that it did not remember the size and position you previously gave it. Lets see how easy it is to fix this.

If your app doesn’t have one already add a global page. I put the code to persist and restore the dialog size and position on the global page so that it works for all modal dialog pages in the app.

Edit the global page (typically page 0) and click the Dynamic Actions tab. Right click and choose Create Dynamic Action. Enter these attributes:
Name: persist dialog size
Event: Custom
Custom Event: dialogresizestop dialogdragstop
Selector Type: jQuery Selector
jQuery Selector: body

Dynamic Action Settings

Change the Show action to Execute JavaScript Code. Change the Selection Type to - Select -. Remember to set Fire on Page Load to No. Enter the following JavaScript:

// handle dialog move and resize
var key, value, store,
    dialog$ = $(this.browserEvent.target), // the dialog being moved or resized
    // the dragstop (move) event doesn't include size so get it from the options
    size = this.data.size || 
        { width: dialog$.dialog("option", "width"),
         height: dialog$.dialog("option", "height") },
    pos = this.data.position;

// Only save the state of apex dialogs.
// Give your dialogs their own class if you want to target specific dialogs
if (dialog$.parents(".ui-dialog--apex").length) {
    // store the dialog state under a key that includes 
    //   the app id, page id, and dialog page id
    value = [size.width, size.height, pos.left, pos.top];
    key = dialog$.find("iframe").attr("src");
    key = "dlg_" + key.split(":")[1]; // get the page of the dialog being opened
    // save the dialog state in browser session storage
    store = apex.storage.getScopedSessionStorage( {
        prefix: "DialogSize", usePageId: true} );
    store.setItem(key, value.join(":"));
}

This dynamic action will save the size and position of any APEX modal dialog page to DOM session storage when it changes. The next step is to restore the size and position when that same dialog is opened again.

Still on the global page, create another Dynamic Action with these attributes:
Name: restore dialog size
Event: Custom
Custom Event: dialogcreate
Selector Type: jQuery Selector
jQuery Selector: body

Dynamic Action Settings

Change the Show action to Execute JavaScript Code. Change the Selection Type to - Select -. Remember to set Fire on Page Load to No. Enter the following JavaScript:

var key, value, store,
    dialog$ = $(this.browserEvent.target); // this is the dialog created

// Only restore state for apex dialogs
if (dialog$.parents(".ui-dialog--apex").length) {
    // the key includes the dialog page that just opened
    key = dialog$.find("iframe").attr("src");
    key = "dlg_" + key.split(":")[1];
    store = apex.storage.getScopedSessionStorage( {
        prefix: "DialogSize", usePageId: true} );
    value = store.getItem(key);
    if (value) {
        value = value.split(":");
        if (value.length === 4) {
            dialog$.dialog("option", "width", parseInt(value[0], 10));
            dialog$.dialog("option", "height", parseInt(value[1], 10));
            dialog$.dialog("option", "position", 
                           [parseInt(value[2], 10), parseInt(value[3], 10)]);
        }
    }
}

[Update 24-Jul-2020 The way jQuery UI dialog works has changed and setting the position no longer works as shown in the example above.
Try something like this:

 ... .dialog("option", "position", 
  { my:  "left top", at: "left+" + parseInt(value[2], 10) + " top+" + parseInt(value[3], 10), of: window } );

]

That is all there is to it. Now run the app again and open, position, and size dialogs. Notice that the next time you open the dialog it displays at the size and position you last set. Because session storage is used to save the dialog state as soon as the browser window or tab is closed the data is lost and you will have to size and position the dialogs again.

A few points about these dynamic actions:

  • These events are jQuery UI dialog events see the jQuery UI documentation for details.
  • This is an example of delegated event handling. The events are set on the body element rather than on each dialog. This is necessary because at the time the events are registered the dialog elements don’t even exist. The events are triggered on the dialog elements but they bubble up to the body.
  • Notice that the first dynamic action responds to two events because the custom event property lists two events separated by a space. When you want to run the same code for multiple events it is nice that you can do this. It should even work if one or more of the events is not custom. You just have to figure out what internal event name corresponds to the event in the drop down list.
  • By adding the dynamic actions to the global page they are added to all pages of the app. Use conditions to control which pages they are added to. Or you could put these dynamic actions on specific pages if your app only has one or two pages that can open dialogs. Adding dynamic actions to the global page only works if the elements that the selector targets exists on each of the intended pages. This example is an ideal case because every page as a body element for this delegated event handler.

[UPDATE Jan-2017 these APIs are now documented in 5.1]
Notice that this code uses the undocumented apex.storage.getScopedSessionStorage API (there is a similar getScopedLocalStorage API). Normally I would caution you about using undocumented APIs but in this case if you are going to use DOM storage (session storage or local storage), I personally would rather you use these APEX APIs than not. The reason is that DOM storage is associated with the page origin. These APIs scope the session keys to the specific APEX app and optionally the page or even region. This is especially important in an environment where multiple workspaces are hosted on a single APEX instance.

A few things to consider before deciding to use DOM storage:

  • Don’t use DOM storage when the user would expect the data to be available regardless of which browser is being used. In this case use APEX_UTIL.SET_PREFERENCE or some other server side persistence.
  • Don’t use localStorage when sessionStorage would do. If it is a UI presentation state setting then sessionStorage is better. The nice thing about session storage is that it doesn’t leave data on the users computer for long periods of time.
  • Never put any sensitive information into sessionStorage or localStorage!
  • You can’t rely on DOM storage being available. Most browsers support it but there are ways to turn it off.
  • Don’t store too much data. Even though DOM storage space is larger than cookies it is still finite. Cleanup after yourself. Don’t leave unneeded keys laying around.
  • Don’t trust the data you read from DOM Storage. Always validate it. Some other code may have changed the values since you last set them or the user could have cleared all the localStorage.

I choose session storage for this feature because if the settings were persisted on the server it would take too long to fetch them from the server when the dialog opened again. Even if a way could be found to render the saved size and position information into the dialog page it would still be too late because the dialog is shown before the page is loaded into the iframe, so server side persistence is not a viable option.

This example is specific to APEX modal dialog pages but it could be modified to work with inline modal dialog regions. One reason that it is not as necessary, is that inline modal dialogs will remember their last size and position as long as you don’t leave the page. This is because inline dialog widgets are reused rather than created each time they are opened and then destroyed on close.

2 Comments

  1. John Snyders
    John Snyders Friday, June 26, 2015

    Hi Jos,
    I didn’t reply to your OTN question (yet) because I don’t think this is the complete answer but I’m glad it helps. In fact your question is what got me thinking about trying this.
    Thanks,
    -John

  2. Jos Dols
    Jos Dols Friday, June 26, 2015

    Hi John,

    Thank you for sharing this.

    I posted 2 questions with this toppic on OTN but no one responsed.

    And then, this/your post came along and save my day!

    I posted the url to this article in my OTN questions, so “more days will be saved”

    Jos

Comments are closed.