Resizing Touch UI component dialogs

Resizing Touch UI component dialogs

In this blog post I lay out a technique that I've recently put together to have more control over component dialog "windows" or modals in AEM 6.1's Touch UI interface. This user-interface is set to replace Classic UI. It's thus a good idea to get acquainted with the way it works. The good news is that by ditching the monolithic ExtJS framework and embracing more modern web technologies (jQuery, HTML5, CSS3), Coral UI and Granite UI offer the web developer a wider entry into customizability. Moreover, the web developer, in my experience, is better equipped in these technologies.

The problem

One of the things that we had in Classic UI was the ability to set custom heights and widths on dialog xtypes. With the concept of xtypes gone, we have to find an alternative way to resize dialogs in Touch UI. We can accomplish this with jQuery and Granite.

The default size of component dialogs in Touch UI is small. This is especially evident on desktop-size clients:

Feels a little cramped. I wanted a way to control the size of these dialogs, while still supporting the fluidity and responsiveness of the out-of-the-box design.

It's also important to make note of the fullscreen toggle. That toggle is a way for a dialog to occupy the full area of a client's viewport, that is, the browser or the screen. This mode kicks in automatically on lower-width viewports. It's also a convenient toggle when a component dialog is more complex and requires focus on the author's part. As web developers in 2016 we should always be thinking of the full spectrum of viewports in our work, whether it's new development or customizations on existing platforms.

The solution

I created a client library node in our project's app folder. Let's say, /apps/bananas/widgets/clientlib_overlay. I name the client library folder this way because it indicates that it affects the Overlay layer in Touch UI. Read Adobe's doc page on how layers are organized.

The client lib is defined, in XML, as:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:ClientLibraryFolder"
    dependencies="[granite.jquery,underscore]"
    embed="[apps.bananas.authoring.editor]"
    categories="[cq.authoring.editor.hook]"/>

The cq.authoring.editor.hook category is a system-defined clientlib category that, no coincidence, allows us to hook our client code into it. About that embed property: a strategy I like to employ is to write component-specific authoring clientlibs inside the component folder, keeping everything about that component as physically close together as possible. The client lib above then, will suck out (that's a technical term) all of the component-specific clientlib code into it, giving us a single request for custom authoring CSS and JS.

The Javascript code

Why Javascript and not just CSS? After all, we could just target the dialog DOM element and set the height and width on all the required elements. Developing Javascript allows us to dynamically and selectively resize the dialog, by setting width and height as JCR properties on component dialog nodes. It's a more flexible approach, but one which requires a little more investment.

Next, we do the needful by setting up our js.txt and js folder in the clientlib. Then we create, inside the js folder, a file that I've called dialog-sizing.js:

(function (document, $) { // (1)
    'use strict';

    $(document).on('dialog-ready', function (e) { // (2)

        // a constant we use throughout
        var INHERIT = 'inherit';

        // our dialog!
        var $dialog = $('.cq-dialog-floating'); // (3)
        if(!$dialog) {
            console.error('Could not get the dialog');
            return;
        }

        var componentPath = $dialog.attr('action').replace('_jcr_content', 'jcr:content');
        var isPageProperties = endsWith(componentPath, 'jcr:content');

        // switch between page properties dialogs and component dialogs
        if(isPageProperties) { // (4)

            // (5)
            var resourceType = Granite.author.page.info.pageResourceType;
            var dialogPath = '/apps/' + resourceType + '/cq:dialog';

            getAndSetSizes(dialogPath, $dialog) // (5.1)
                .fail(function() { // if we fail, try the dialog of the resourcesupertype (5.2)
                    var componentPath = '/apps/' + resourceType;
                    $.getJSON(componentPath + '.json').done(function (data) {
                        var dialogPath = '/apps/' + data['sling:resourceSuperType'] + '/cq:dialog';
                        getAndSetSizes(dialogPath, $dialog);
                    });
                })

        } else {
            // (6)
            // the list of Editables (an Editable is an instance of a component on a Page)
            var editables, error = true;
            if(Granite.author) {
                editables = Granite.author.store;
                error = typeof editables === 'undefined';
            }

            if(error) {
                console.error('Could not fetch the Editables');
                return;
            }

            // loop over each editable and find the one that matches to the currently open dialog
            $.each(editables, function(index, value) {
                if(value.path == componentPath) { // (6.1)
                    // the path to the component dialog, which holds the width and height properties
                    var dialogPath = value.config.dialog;
                    getAndSetSizes(dialogPath, $dialog); // (6.2)
                    return false; // break out of $.each
                }
            });
        }

        /**
         * Gets the sizes from the properties at dialogPath, and sets those dimensions on the
         * $dialog element
         *
         * @param $dialog
         * @param dialogPath
         * @returns {*} the jqxhr object of the request
         */
        function getAndSetSizes(dialogPath, $dialog) { // (7)
            return $.getJSON( dialogPath + '.json')
                .done(function(data) {
                    setDialogSize($dialog, data.width, data.height);
                    addFullscreenToggle($dialog, data.width, data.height);
                });
        }

        /**
         * Sets the width and the height on the dialog, the dialog's content element(s), and re-centers
         * the dialog.
         *
         * @param $dialog
         * @param width
         * @param height
         */
        function setDialogSize($dialog, width, height) { // (8)

            if(width) {

                // set the width!
                $dialog.css('width', width);

                var smallerWidth = parseInt(width) - 5;
                var matches = width.match(/[a-zA-z]+/);
                if(matches) {
                    var unit = matches[0];
                    $dialog.find('.coral-FixedColumn > .coral-FixedColumn-column').css('width', smallerWidth + unit);
                } else if(width === INHERIT) {
                    $dialog.find('.coral-FixedColumn > .coral-FixedColumn-column').css('width', width);
                }
            }

            if(height) {

                // For fixed column layouts
                $dialog.find('.coral-FixedColumn').css('height', height);

                // For tabbed layouts
                $dialog.find('.coral-TabPanel .coral-TabPanel-content').css('height', height);

                // add more layouts here!
            }

            // force repositioning of the dialog by triggering this event, see
            // /libs/cq/gui/components/authoring/clientlibs/editor/js/DialogFrame.js
            $(document).trigger('cq-sidepanel-resized.dialogframe');
        }

        /**
         * When switching back/to fullscreen in the dialog, toggle between the custom and inherited
         * dimensions on the $dialog.
         *
         * @param $dialog
         * @param width
         * @param height
         */
        function addFullscreenToggle($dialog, width, height) { // (9)
            //
            $(document).on('click', '.cq-dialog-layouttoggle', function(e) {
                e.preventDefault();
                if($dialog.hasClass('cq-dialog-fullscreen')) {
                    setDialogSize($dialog, INHERIT, INHERIT);
                } else {
                    setDialogSize($dialog, width, height);
                }
            });
        }

        /**
         * Tests if a string ends with another string
         *
         * @param str
         * @param suffix
         * @returns {boolean}
         */
        function endsWith(str, suffix) {
            return str.indexOf(suffix, str.length - suffix.length) !== -1;
        }
    });

})(document, Granite.$);

JS breakdown

Let's talk about each point identified in the code.

1 - This structure

(function (document, $) {
    'use strict';

    // ...

})(document, Granite.$);

is becoming common-place in AEM 6.1 client-side code. You'll notice this approach all over libs. It's basically an anonymous self-executing function that's scoped-limited in which we pass dependencies. In this case, we pass our document and the jQuery object affected to the Granite object. This object is obviously provided by Granite UI.

2 - This is the event we hook into that lets us know that a wild component dialog has appeared!

3 - This is the jQuery object of the dialog. It exists in the DOM and has all sorts of good information attached to it.

4 - One of those goodies is info on where the dialog submits to. From this we can deduce if it's a dialog for a page-component or not.

5 - In both cases, we need to know the resource type of our component, and the path to its dialog. The latter is where we persist dialog width and height (more on this in a bit), so we need to know where to fetch them!

5.1 - This is the call to our getAndSetSizes() which will carry out the magic.

5.2 - Exclusively for page-components, upon failure, we try again using the page-component's super type (i.e. the parent component). Who knows, we might strike gold.

6 - Here, for regular components, we start by getting the list of Editables. Each Editable object represents a component on our page containing, once again, a wealth of information on the client side.

6.1 - We iterate over all the Editables looking for the component path that matches our dialog's path.

6.2 - When we have a match, we call the same getAndSetSizes() to kick off the resizing.

7 - In this function, we do an ajax request against the component's dialog JSON. There we will find our height and width properties (or not) and we proceed by calling the function that will touch the DOM.

8 - Here, we evaluate the arguments passed, and if width and height exist, respectively, they will be used in affecting the CSS of the dialog. There are some peculiarities with different layouts (as defined by Granite), so as you encounter and use more, they will need some special massaging here.

9 - Here, we switch between custom height and widths and the inherit CSS property, so that in full screen, our custom values are not applied.

Setting width and height in the component dialog

The last thing to do is to set the desired width and height on our dialog! The Javascript code above supports any CSS unit, but for best results, let's respect the rem unit set by Coral and Granite UIs. In our component dialog, node cq:dialog.xml (which, pro-tip, can simply be represented as file _cq_dialog.xml in your component folder instead of folder/file pair: _cq_dialog/.content.xml -- saving us mouse clicks, one folder at a time!)

So anyway, our dialog, in XML form, would look like:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="Page"
    sling:resourceType="cq/gui/components/authoring/dialog"
    width="50rem"
    height="30rem"
    mode="edit">
    <content>
      ...
    </content>
  </jcr:root>

Now, when we invoke our dialog, we get a wider, taller dialog, giving us breathing room for options and configurations, and making for a better author experience:

That's it! I hope the techniques and code laid out here is useful for your Touch UI development. Don't hesitate to post a comment if anything's unclear, or if my code can be improved!

Share this post

0 Comments

comments powered by Disqus