Extending the SDL Tridion 2011 Rich Text Field Format Toolbar

The Problem

A client recently asked for a product demo. They had specific scenarions to be followed, including the creation/insertion of mathematical equations. The sample content suggested that they wanted the equations to be displayed inline with textual content. In other words, the equations would need to be added to the source of a rich text field.

I figured that if I could find an equation editor that would run in an HTML page then I could somehow extend the RTF format toolbar to launch the functionality, then inserting whatever the editor returned in to the RTF at the location of the cursor. It was actually very easy to find a suitable editor – CodeCogs have a javascript-based editor that can be embeded in an HTML page which creates image representations of the described equations. Having found the right editor I then had to figure out how to extend the toolbar to enable the functionality. A quick email to R&D provide me with the lead I needed – the toolbar can be extended in the same way you would extend a ribbon toolbar elsewhere in the interface.

Knowing where I was going to, and knowing which direction to start off in, but not knowing the exact route I was going to follow to get to my destination, I set off. I achieved a moderate degree of succuess – adding a button to the toolbar which opened a popup containing the editor, and returning a fixed string of HTML to the RTF. Unfortunately some conflict between the Tridion javascript and the 3rd party javascript meant that I couldn’t get the end-to-end process to work completely, but it did give me an understanding of how to approach this type of extension. So that the effort was not all in vain I refactored what I’d created to deliver a reference implementation.  The key elements of the extension are detailed below.

Solution

Adding the toolbar button

There are plenty of examples available that show how to add a button to a ribbon toolbar in SDL Tridion 2011, so I won’t go in to the details of this. There is some specific configuration though, so I will cover this. I wanted to create a button in a custom group on the format toolbar that appeared only when editing Components. This is how it’s configured:

<ext:ribbontoolbars>
    <ext:add>
        <!-- RIBBON TAB -->

        <!-- GROUPS -->
        <ext:extension assignid="ExtensionGroup" pageid="FormatPage" name="RTF Extensions">
            <ext:group/>
            <ext:apply>
                <ext:view name="ComponentView">
                    <ext:control id="ItemToolbar"/>
                </ext:view>
            </ext:apply>
        </ext:extension>

        <!-- BUTTONS -->
        <ext:extension pageid="FormatPage" groupid="ExtensionGroup" name="Button&lt;br/&gt;Reference" assignid="ButtonReference">
            <ext:command>ButtonReference</ext:command>
            <ext:title>Button Reference</ext:title>
            <ext:dependencies>
                <cfg:dependency>RTFExtensions.ButtonReference.Commands</cfg:dependency>
            </ext:dependencies>
            <ext:apply>
                <ext:view name="ComponentView">
                    <ext:control id="ItemToolbar"/>
                </ext:view>
            </ext:apply>
        </ext:extension>
    </ext:add>
</ext:ribbontoolbars>

You can see from this config snippet that I configured an RTF Extensions group with the ID “ExtensionGroup”, associated with the “FormatPage” – this is the ID of the Format ribbon tab, on which my group, and the button will appear. The button is also associated with “FormatPage”, as well as my custom group. The final part of the picture is where the group and the button are applied. I specified the “ComponentView” view as I wanted this to only appear when editing a Component. For the extension to appear for all item types apparently the name attribute can be omitted. For other item types, just specify the appropriate view name, e.g. PublicationView, FolderView, etc.

Controlling the availability of the button

Once I had the button appearing when I wanted it to, the next step was to make sure it was only active when the RTF was active. Having been use to configuring extension availability based on the selection of an item in a list view I was stuck again on how to proceed. Another email to R&D pointed me towards the existing toolbar button implementations to see how it’s done out-of-the-box. This is what I was able to find:

RTFExtensions.Commands.ButtonReference.prototype._isAvailable = function ButtonReference$_isAvailable(target) {
    if (target.editor.getDisposed()) {
        return false;
    }

    return true;
};

RTFExtensions.Commands.ButtonReference.prototype._isEnabled = function ButtonReference$_isEnabled(target) {
    if (!Tridion.OO.implementsInterface(target.editor, "Tridion.FormatArea") || target.editor.getDisposed()) {
        return false;
    }

    return true;
};

There was a little more to it than that, but this is what was relevant to my extension. Essentially the “selection”, or target is related to the cursor location, and contains the RTF editor. I didn’t investigate, but I assume that if I’d selected a block of text then the target would reference this.

Opening the popup

This will probably be the shortest part of this post as opening a popup is easy – I just used the $popup object. What looked like it would be tricky would be to get hold of the HTML created in the popup so it could be insterted in to the RTF at the cursor. More emails to R&D had me checking out more of the existing code used by the CME.

Getting the return value from the popup

It turns out that this is relatively straight-forward, but is handled in 2 parts. The first part is to “prime” the popup to provide the return value, which is done before the popup is opened. The second part is to initiate the return of the value, which is handled with in the popup. Both parts are tied to events.

Here’s the code needed before the popup is opened (in fact, the complete execute function):

RTFExtensions.Commands.ButtonReference.prototype._execute = function ButtonReference$_execute(target) {
    if (target.item.isActivePopupOpened()) {
        return;
    }

    function ButtonReference$execute$onPopupCanceled(event) {
        target.item.closeActivePopup();
    };

    var url = $config.expandEditorPath("/Popups/PopupReference.aspx", "ButtonReference");
    var popup = $popup.create(url, "toolbar=no,width=100,height=100,resizable=yes,scrollbars=yes", null);

    $evt.addEventHandler(popup, "submit",
        function ButtonReference$execute$onPopupSubmitted(event) {
            // Update FA
            var value = event.data.value;
            if (value) {
                target.editor.applyHTML(value);
            }

            // Release
            target.item.closeActivePopup();
        }
    );

    $evt.addEventHandler(popup, "unload", ButtonReference$execute$onPopupCanceled);

    target.item.setActivePopup(popup);
    popup.open();
};

You can see that I’m setting a function to handle the submit event. This function gets the return value from the event data and applies it to the selected RTF editor. To compliment this code, the javascript for the popup needs to fire the submit event. Here’s the code that sets the return value and triggers the event:

Type.registerNamespace("RTFExtensions.Popups");

RTFExtensions.Popups.PopupReference = function (element) {
    Type.enableInterface(this, "RTFExtensions.Popups.PopupReference");
    this.addInterface("Tridion.Cme.View");
};

RTFExtensions.Popups.PopupReference.prototype.initialize = function () {
    $log.message("Initializing Button Reference popup...");
    this.callBase("Tridion.Cme.View", "initialize");

    var p = this.properties;
    var c = p.controls;

    p.HtmlValue = { value: null };

    c.InsertButton = $controls.getControl($("#InsertButton"), "Tridion.Controls.Button");
    $evt.addEventHandler(c.InsertButton, "click", this.getDelegate(this._execute));
};

RTFExtensions.Popups.PopupReference.prototype._execute = function () {
    this.properties.HtmlValue.value = "<p><u>Sample string of HTML</u></p>";
    this.fireEvent("submit", this.properties.HtmlValue);
    window.close();
};

$display.registerView(RTFExtensions.Popups.PopupReference);

You can see that a button control on the popup page is referenced, an event handler is added to deal with the button being clicked, and in the click-executed function the submit event is fired with the HTML to be added to the RTF as a parameter.

What’s in the popup?

The source below is from the popup page in my reference implementation. The important parts to note are the Tridion UI controls namespace reference in the html element and the button (that uses the reference). The button ties back to the javascript in the previous section.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="PopupReference.aspx.cs" Inherits="Button.Reference.Popups.PopupReference" %>

<%@ Import Namespace="Tridion.Web.UI.Core" %>
<%@ Import Namespace="Tridion.Web.UI" %>
<%@ Import Namespace="System.Web" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:c="http://www.sdltridion.com/web/ui/controls">

<head id="Head1" runat="server">
    <title>Reference Button Popup</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h1>Reference Button Popup</h1>
        <p>
            <c:button id="InsertButton" runat="server" label="Insert" />
        </p>
    </div>
    </form>
</body>
</html>

The popup implementation javascript and the Tridion core dependencies are added in the code-behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using Tridion.Web.UI.Controls;
using Tridion.Web.UI.Core.Controls;

namespace Button.Reference.Popups
{
    [ControlResourcesDependency(new Type[] { typeof(Popup), typeof(Tridion.Web.UI.Controls.Button), typeof(Stack), typeof(Dropdown), typeof(List) })]
    [ControlResources("RTFExtensions.ButtonReference")]
    public partial class PopupReference : TridionPage
    {
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            TridionManager tm = new TridionManager();

            tm.Editor = "RTFButtonReference";
            System.Web.UI.HtmlControls.HtmlGenericControl dep = new System.Web.UI.HtmlControls.HtmlGenericControl("dependency");
            dep.InnerText = "Tridion.Web.UI.Editors.CME";
            tm.dependencies.Add(dep);

            System.Web.UI.HtmlControls.HtmlGenericControl dep2 = new System.Web.UI.HtmlControls.HtmlGenericControl("dependency");
            dep2.InnerText = "Tridion.Web.UI.Editors.CME.commands";
            tm.dependencies.Add(dep2);

            //Add them to the Head section
            this.Header.Controls.Add(tm); //At(0, tm);
        }
    }

The line “[ControlResources(“RTFExtensions.ButtonReference”)]” links the popup to the javascript and style references in the extension config.  The code in OnInit adds the relevant core dependencies. Without these some of API calls made, e.g. .fireEvent, won’t work.

Wrap-up

Hopefully this post has helped to explain how to add a RTF toolbar button. It’s not meant as a step-by-step tutorial, more as a introduction to some of the steps implemented to make an RTF button extension work. The source code has been shared through the Tridion Practice site on Google Code. Feel free to download the source and play with it.

Advertisements

SiteEdit 2009 & JavaScript trickery

Problem

Recently I was preparing a demo for a prospect. Part of what I was doing involved making one of their product pages siteeditable. The page in question contained a lot of content, but the presentation was simplified through the use of tabs. The client had used prototype to achieve this, although I don’t believe this had any specific impact on the problem this caused. I wanted to be able to edit all of the content from all of the tabs, however this was proving somewhat difficult – only the first visible tab was outlined correctly, and when other tabs were selected the original outlining remained. I needed to find a way to make all of the content visible.

Analysis

Reading the description of the problem no doubt there are those of you who are saying “easy, just change the design to show all the tabbed content all of the time” and , of course, this is one solution that would work. However, I wanted to maintain the integrity of the design, so I needed a solution that would only make the tabbed content temporarily available.

The direction I needed to go in was obvious – with JavaScript being used to hide the content in the tabs, and with SiteEdit being largely JavaScript-based, my solution was to be a JavaScript solution. So far so good, but how was I going to get my JavaScript to run only when I wanted it to? With maintaining the design integrity still clear in my mind I decided I needed a solution that would only trigger when SiteEdit was active, so I  needed a way to identify when this was true.

Fortunately working for SDL means that I have access to resources in R&D that are not only in a position to offer advice, but are also happy to do so. After a flurry of emails, and a couple of false starts (apparently it’s really easy to determine if you’re viewing the staging site through the SE proxy or not), the pieces of the puzzle began to fall in to place.

Solution

My solution used SiteEdit events to capture when the Component Presentation was selected (clicked) – literally the “componentPresentationClicked” event:

function pageInit() {
    // Special functionality when CP selected
    var onComponentPresentationClicked = function() {
        // Code goes here
    }

    // if SiteEdit is active (Page is viewed through proxy) attach special functionality
    if (typeof(top.$SeEventManager) != "undefined") {
        var m = top.$SeEventManager.getInstance();
        m.Subscribe("componentPresentationClicked", onComponentPresentationClicked);

        try {
            Event.observe(window, 'unload', function() {
                m.Unsubscribe("componentPresentationClicked", onComponentPresentationClicked);
            });
        } catch (err) {
            // probably should do something here...
        }
    }
}

document.observe("dom:loaded",function() { pageInit(); });

In the end it was straight-forward to achieve, although I’ve since learned that there might be a more appropriate event to hook in to, but hopefully this will be useful information. And don’t forget, if you’re trying anything like this then Firebug is your friend 😉