Customizing An Existing Tridion Popup Dialog

Ever had a need or an itch to completely modify and customize one of those default Tridion popup dialogs? Perhaps you even had a desire to overload an existing JavaScript function from one of those dialogs, but didn’t want to just copy/paste or replace the existing one entirely? Today I’ll be giving you a walk through of adding some custom fields and functionality to the Link popup. Before I begin, I want to give a special thanks to Frank Taylor as we worked on this kind of functionality together not too long ago. And by working together, I mean that I merely made some suggestions and he did all the work so that one day I could steal it as a reference point and write the post you are reading today. So if you find anything on this page useful, feel free to also give Mr. Frank a shout out! And respectively, if you think this article is complete crap and the code here somehow murders your beloved cat, feel free to leave a burning bag of poo on his front door step. Just ping me and I’ll give you his personal home address.

If you’re not interested in reading and just want the code that was done in this post, you can find it here.

The Mission

Our mission today is to add the ability for content authors to add extra custom attributes onto their links in rich text fields via the Hyperlink button in the ribbon bar.  For the sake of simplicity, we’ll only add a single set of a key value pair today, and save this enhanced feature for a post for another day.  Also, today’s post will only focus on the GUI Extension piece of this puzzle as the point of today’s blog is more focused on the “how do I customize an existing dialog?”

Analysis

At first thought, we could apply our custom link attribute directly to the link elements from the rich text field.  However, these would just get stripped away in an out of the box Tridion setup due to being invalid attributes.  Although we could configure Tridion to prevent this from happening (for information on how to do this, check Nick’s post here), I wanted to take a more generic approach where we could add attributes regardless of this configuration.  So what we’ll be doing instead is adding these special link attributes as hash parameters to the href attribute.  We’ll give them a special prefix of “linkAttr-” to distinguish our hash parameters from any that may already be a part of the url supplied.  This means that we’ll also need to create a Template Building Block that will search for these specially marked hash params, turn them into attributes on the link element, and remove them from the hash string.  As mentioned earlier, since we’ll only be focusing on the GUI Extension piece today, we’ll leave that for the next blog post.

Assumptions

I’m going to assume that you are already somewhat familiar with developing Tridion GUI Extensions, and will only be going over code and configuration that is specific to this extension. If you are looking for a tutorial that teaches you how to create a Hello World from scratch, there’s already quite a few good ones out there. We’re also going to assume that we’ll be using jQuery and that one doesn’t already exist. We’ll be adding jQuery to a global $jq variable.

The JavaScript

We’re going to start by creating a utility class, “HashCollection”.  This class will be responsible for extracting the hash parameters from a given url, as well as maintaining, adding and clearing of our link attributes.

Type.registerNamespace("ContentBloom.Extensions");

/**
 * HashCollection is a collection of styles on a link's hash parameters.  This class is responsible
 * for getting, setting, and clearing link attributes.
 *
 * @constructor
 */
ContentBloom.Extensions.HashCollection = function (url) {
    var hashString,
        params,
        param,
        i;

    /**
     * The link attribute prefix.  This prefix is applied to any hash params that are for link attributes only.
     * @type {string}
     * @private
     */
    this._linkAttributePrefix = "linkAttr-";

    /**
     * Whether or not the hash collection contains link attributes. 
     * @type {boolean}
     */
    hasLinkAttributes: false;

    /**
     * The original url passed into the HashCollection instance.
     * @type {string}
     */
    this.url = url;

    /**
     * The params on the url
     * @type {object[]}
     */
    this.params = [];

    if (url.indexOf("#") !== -1) {
        hashString = url.substring(url.indexOf("#") + 1);

        params = hashString.split('&');
        for (i = 0; i < params.length; i++) {
            param = params[i].split('=');
            this.params[this.params.length] = { property: param[0].trim(), value: param[1].trim() };
            if (!this.hasLinkAttributes && param[0].trim().indexOf(this._linkAttributePrefix) === 0) {
                this.hasLinkAttributes = true;
            }
        }
    }
};

// StyleCollection prototype members
ContentBloom.Extensions.HashCollection.prototype = {

    /**
     * Clears all params from the collection that are specific to Link Attributes.
     */
    clearLinkAttributes: function () {
        var length = this.params.length,
            param,
            i;

        while (length--) {
            param = this.params[length];
            if (param.property.indexOf(this._linkAttributePrefix) === 0) {
                this.params.splice(length, 1);
            }
        }
        this.hasLinkAttributes = false;
    },

    /**
     * Gets the hash string (starting with '#') only of the wrapped url. Supplies link attribute hash params at the end of the string.
     *
     * @returns {string}
     */
    getHashString: function () {
        var hashString = "#",
            linkAttributes = "",
            length = this.params.length,
            param,
            i;

        if (length === 0) {
            return "";
        }

        for (i = 0; i < length; i++) {
            param = this.params[i];
            if (param.property.indexOf(this._linkAttributePrefix) === 0) {
                if (i > 0) {
                    linkAttributes += "&";
                }
                linkAttributes += "{0}={1}".format(param.property, param.value);
            } else {
                if (i > 0) {
                    hashString += "&";
                }
                hashString += "{0}={1}".format(param.property, param.value);
            }
        }


        return hashString + linkAttributes;
    },

    /**
     * If a link attribute property is supplied, strips the prefix and returns just the key. Else just reutrns the key.
     *
     * @returns {string}
     */
    getKey: function (key) {
        if (key.indexOf(this._linkAttributePrefix) === 0) {
            return key.substring(9);
        }
        return key;
    },

    /**
     * Gets a link attribute param by its key.
     *
     * @param {string} key - the key to retrieve (excluding the link attribute prefix)
     * @returns {Object|null} null if no attribute is found with given key.
     */
    getLinkAttribute: function (key) {
        var length = this.params.length,
            param,
            i;

        for (i = 0; i < length; i++) {
            param = this.params[i];
            if (param.property === this._linkAttributePrefix + key) {
                return param;
            }
        }
        return null;
    },

    /**
     * Gets only the parameters that are link attribute parameters.
     */
    getLinkAttributes: function () {
        var linkAttributes = [],
            length = this.params.length,
            param,
            i;

        if (!this.hasLinkAttributes) {
            return linkAttributes;
        }

        for (i = 0; i < length; i++) {
            param = this.params[i];
            if (param.property.indexOf(this._linkAttributePrefix) === 0) {
                linkAttributes[linkAttributes.length] = param;
            }
        }

        return linkAttributes;
    },

    /**
     * Returns a new url including the hash string generated by any parameters.
     *
     * @returns {string}
     */
    getHashedUrl: function () {
        var url;

        if (this.url.indexOf("#") !== -1) {
            url = this.url.substring(0, this.url.indexOf("#"));
        } else {
            url = this.url;
        }

        return url + this.getHashString();
    },

    /**
     * Sets a link attribute param.  If attribute already exists, updates it.
     *
     * @param {string} key - The key of the link attribute (exluding link attribute prefix).
     * @param {string} value - the value of the link attribute.
     */
    setLinkAttribute: function (key, value) {
        var param = this.getLinkAttribute(key);
        if (param !== null) {
            param.value = value;
        } else {
            this.params[this.params.length] = { property: this._linkAttributePrefix + key, value: value };
        }
        this.hasLinkAttributes = true;
    }

}

Now that we have our utility class, let’s write the code that’s going to be doing the real work.

Type.registerNamespace("ContentBloom.Extensions");

/**
 * Represents functionality for adding custom attributes onto links from the Link Popup dialog.
 */
ContentBloom.Extensions.LinkAttributes = {

    /**
     * The table row in the popup containing the key value pairs of links.
     * @type {JQuery}
     */
    attributeContainer: null,

    /**
     * The url input field.
     * @type {JQuery}
     */
    urlField: null,

    /**
     * Initializes the LinkAttributes object.
     */
    init: function () {
        this.customizeLinkObject();
        $jq('#rowTarget').parent().append('<tr id="customAttributes"><td>Attributes:</td><td id="attributeContainer" colspan="4"></td></tr>');

        this.attributeContainer = $jq('#attributeContainer');
        this.urlField = $jq("#FieldUrl");
        
        this.insertKeyValuePair();
        this.initValues();
    },

    /**
     * Initializes input field values on the gui (url, link attributes...).
     */
    initValues: function () {
        var hashParams,
            linkAttributes,
            attribute,
            length,
            i,
            oldLink = window.dialogArguments && window.dialogArguments.link ? window.dialogArguments.link : {};


        if (oldLink.href) {
            hashParams = new ContentBloom.Extensions.HashCollection(oldLink.href);
            linkAttributes = hashParams.getLinkAttributes();
            length = linkAttributes.length;

            if (hashParams.hasLinkAttributes) {
                this.attributeContainer.html('');
                for (i = 0; i < length; i++) {
                    attribute = linkAttributes[i];
                    this.insertKeyValuePair(hashParams.getKey(attribute.property), attribute.value);
                }
                hashParams.clearLinkAttributes();
                this.urlField.val(hashParams.getHashedUrl();
            }
        }
    },

    /**
     * Gets an array of custom attributes based on text fields in the gui.
     *
     * @returns {object[]}
     */
    getCustomAttributes: function () {
        var attributes = [];

        $jq('div', this.attributeContainer).each(function(index, element) {
            var key = $jq('.key', element).val(),
                value = $jq('.value', element).val();

            if (key) {
                attributes[attributes.length] = { key: key, value: value };
            }
        });

        return attributes;
    },

    /**
     * Inserts a new key value pair of text fields into the attribute container.
     *
     * @param {string=} key - The optional key to set the key input field to.
     * @param {string=} value - The optional value to set the value input field to.
     */
    insertKeyValuePair: function (key, value) {
        if (key == undefined) {
            key = "";
        }
        if (value == undefined) {
            value = "";
        }
        this.attributeContainer
            .append('<div><label>Key</label><input class="key" type="text" value="' + key + 
                '" /> <label>Value</label><input class="value" type="text" value="' + value + '" /></div>');
    },

    /**
     * Modifies the Link._buildNewLinkHtml method to also apply the custom link attributes. Monkey patches, so existing method is still used and not replaced.
     */
    customizeLinkObject: function () {
        var self = this,
            originalFn = Tridion.Cme.Views.Link.prototype._buildNewLinkHtml;
            

        Tridion.Cme.Views.Link.prototype._buildNewLinkHtml = function Link$_buildNewCustomLinkHtml() {
            var link,
                hashParams,
                attributes = self.getCustomAttributes(),
                attribute,
                i;

            originalFn.apply(this);

            hashParams = new ContentBloom.Extensions.HashCollection(this.properties.NewLink.href);
            hashParams.clearLinkAttributes();

            for (i = 0; i < attributes.length; i++) {
                attribute = attributes[i];
                hashParams.setLinkAttribute(attribute.key, attribute.value);
            }

            this.properties.NewLink.href = hashParams.getHashedUrl();
        };
    }
};

// We call init only after document has loaded.
$jq(function () {
    ContentBloom.Extensions.LinkAttributes.init();
});

On Load

It’s important to notice that at the end of this script, we are making a call to our ContentBloom.Extensions.LinkAttributes.init() method only after the document loaded. If we were to call the init method immediately, we’d get some unexpected results.

initValues

You’ll notice here that we check if this is an existing hyperlink that has already been given link attributes. If the hash string contains our specially marked values, we’ll populate or key/value fields appropriately.

Future Proofing

You’ll also notice that although we are only displaying a single set of a key/value pair, our code is already setup to handle multiple sets.

Monkey Patching _buildNewLinkHtml

Here’s another cool trick that we are doing… we are adding functionality to the existing Tridion.Cme.Views.Link.prototype._buildNewLinkHtml method without actually modifying any of the existing code (which would be bad). We’re using a little closure magic here in our customizeLinkObject method to create a copy of the existing function, overwrite the existing function, and ensure that the original function still gets called prior to our custom code (while ensuring that the context of “this” remains for the Link instance).

The Stylesheet

Not too much going on here at this stage of our extension. We’re just making sure out labels stand out… depending on the browser you are on, this may display nicely or get wrapped onto a second line. We’ll come back in another post to pretty-ify it up a lil more though.

/**
 * Yep, not a too lot going on in here just yet...
 */
#LinkPopup #customAttributes label {
    font-weight: bold; // tempted to throw an !important in there just for you Frank...
}

Configuration

Here’s where everything comes into play that allows us to load our newly created JS and CSS files onto the existing Link Popup page. What we want to do here is create an Extension Group. An Extension Group allows us to extend an already existing Group and add additional files to it. In this case, we want to extend the “Tridion.Web.UI.Editors.CME.Views.Popups.Link” group. Since the configuration of a GUI Extension seems to be the biggest gotcha, I’ll go over the process for this extension piece by piece.

...
<?xml version="1.0"?>
<Configuration xmlns="http://www.sdltridion.com/2009/GUI/Configuration/Merge"
               xmlns:cfg="http://www.sdltridion.com/2009/GUI/Configuration"
               xmlns:ext="http://www.sdltridion.com/2009/GUI/extensions"
               xmlns:cmenu="http://www.sdltridion.com/2009/GUI/extensions/ContextMenu">

  <resources cache="true">
    <cfg:filters />
    <cfg:extensiongroups>
      <cfg:extensiongroup name="ContentBloom.ExtensionGroups.Link">
        <cfg:extension target="Tridion.Web.UI.Editors.CME.Views.Popups.Link">
          <cfg:insertafter>ContentBloom.Resources.LinkAttributes</cfg:insertafter>
        </cfg:extension>
      </cfg:extensiongroup>
    </cfg:extensiongroups>
    <cfg:groups>
     <cfg:group name="ContentBloom.Resources.LinkAttributes">
        <cfg:fileset>
          <cfg:file type="script">/js/libs/jquery.js</cfg:file>
          <cfg:file type="script">/js/HashCollection.js</cfg:file>
          <cfg:file type="script">/js/LinkAttributes.js</cfg:file>
          <cfg:file type="style">/css/LinkAttributes.css</cfg:file>
        </cfg:fileset>
      </cfg:group>
    </cfg:groups>
  </resources>
  <definitionfiles />
  <extensions>
      <ext:editorextensions/>
      <ext:dataextenders/>
  </extensions>
  <commands />
  <contextmenus />
  <localization />
  <settings>
    <defaultpage />
    <navigatorurl />
    <editurls/>
    <listdefinitions/>
    <itemicons/>
    <theme>
      <path/>
      <resourcegroup />
    </theme>
    <resourceextensions>
        <resourceextension>ContentBloom.ExtensionGroups.Link</resourceextension>
    </resourceextensions>
    <customconfiguration/>
  </settings>
</Configuration>

Creating the Resource Group

First we create our <cfg:group> element within the <cfg:groups> element. Our resource group is telling Tridion which resource files to include into our little extension… in our case we are loading our JS and CSS files.

Creating the Extension Group

This is where we inject our resources into an already existing resource group. We’ll need to create a <cfg:extensiongroup /> element within the <cfg:extensionsgroups /> element with a unique name attribute.  Within the <cfg:extensiongroup /> element, you’ll want to create a <cfg:extension /> element that has a target attribute that contains a value of the existing resource group that we want to extend (here we are extending “Tridion.Web.UI.Editors.CME.Views.Popups.Link”).  And within that <cfg:extension /> element, you’ll want to create a <cfg:insertafter /> element that contains the name of the resource group that we created in the above step.

Don’t Forget the Resource Extension!

One final piece to this configuration… in the settings element, we want to create a <resourceextensions> element that contains a <resourceextension> element. The value of this element should be that unique name that we gave to our extensiongroup that we created above.

Deploy!

And there you have the starts of the Link Attributes extension! You will have to deploy the extension as normal… a.) virtual directory creation… b.) Add the editor configuration in System.config… c.) Increment the modification attribute. Open up a component with a rich text field, create a Hyperlink using the Hyperlink button, and you should be in business! You may not yet be able to create actual link attributes, but at least you’ll be able to see your customizations as well as modified href attribute on the links!

Our extension of version 1 in action!

Our extension of version 1 in action!

Conclusions

Although our work for this Link Attributes Extension is not yet complete, you should have now be well armed in case you ever have to customize any of the default popup dialogs.

And for those of you who are actually interested in the completion of this extension, stay tuned for future posts where we’ll work on the TBB, as well as add the ability to add multiple link attributes (and fix up some issues that you may have noticed).

You can find the code to what we’ve done today here.

Stay coding my friends!

Updates

8/21/2014 – Follow up post for building out the Template Building Block

3 thoughts on “Customizing An Existing Tridion Popup Dialog

  1. Hi, When I implement this, this extension is executed first than the default functionality (Link), with this window.dialogArguments is undefined and initValues is not executing properly.

    • Thanks for reporting… this was originally built for 2011, it looks like in 2013 you’ll have to do the init on the $display “start” event… I’ll be adding this repository to github this week with the updates, but in the mean time you can do a quick fix in LinkAttributes.js. At the bottom at the file, replace the jQuery load event with something like:


      function startLinkAttributeCustomization() {
      ContentBloom.Extensions.LinkAttributes.init();
      $evt.removeEventHandler($display, "start", startLinkAttributeCustomization);
      }

      $evt.addEventHandler($display, "start", startLinkAttributeCustomization);

  2. Pingback: Experience manager Gui extensions, insert text into a field rather than replace | DL-UAT

Leave a Reply to Vamsi Krishna Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>