Adding to the CKEditor context menu

We are using the excellent CKEditor in some of our Visualforce pages now, partly to get around the problem that standard rich text fields error when they are re-rendered. But explicitly adding the editor also makes it cleaner to leverage its many features and the one described here is the ability to extend the context (right click) menu. I couldn’t find a simple example, so I hope this post will act as that for others.

The JavaScript code is below and works on this text area:

<apex:inputTextArea value="{!ref.Rtf__c}" styleClass="ckeditor" richText="false"/>

The purpose of the menu is to allow fragments of text to be inserted when selected via the context menu. (The actual JavaScript menu code is much longer and generated by Apex code using data returned by describe calls; it is also loaded from a static resource so that it can be cached by the browser.)

Here is a screen shot from the code posted here:

Menu (1)

A brief explanation of the code is that the menu items either return other menu items (when they are the root menu or a sub-menu) or reference a command (when they are a leaf action). The menu groups add dividing lines into the menus. The editor is added based on the ckeditor class name.

The code:

<script src="//cdn.ckeditor.com/4.5.9/standard/ckeditor.js"></script>
<script>

// Menu code
function addMergeFieldMenu(event) {
    var e = event.editor;
    
    // Commands
    e.addCommand("cv_claimant_Birthdate", {
        exec: function(e) {
            e.insertText("\{\!claimant.Birthdate\}");
        }
    });
    e.addCommand("cv_claimant_Name", {
        exec: function(e) {
            e.insertText("\{\!claimant.Name\}");
        }
    });
    e.addCommand("cv_claim_Name", {
        exec: function(e) {
            e.insertText("\{\!claim.Name\}");
        }
    });
    e.addCommand("cv_claim_CreatedDate", {
        exec: function(e) {
            e.insertText("\{\!claim.CreatedDate\}");
        }
    });
    
    // Listener
    e.contextMenu.addListener(function(element, selection) {
        return {
            cv: CKEDITOR.TRISTATE_ON
        };
    });
    
    // Menu Groups; different numbers needed for the group separator to show
    e.addMenuGroup("cv", 500);
    e.addMenuGroup("cv_people", 100);
    e.addMenuGroup("cv_claim", 200);
    
    // Menus (nested)
    e.addMenuItems({
        // Top level
        cv: {
            label: "Insert Merge Field",
            group: "cv",
            getItems: function() {
                return {
                    cv_claimant: CKEDITOR.TRISTATE_OFF,
                    cv_claim: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // One sub-menu
        cv_claimant: {
            label: "Claimant Person (claimant)",
            group: "cv_people",
            getItems: function() {
                return {
                    cv_claimant_Birthdate: CKEDITOR.TRISTATE_OFF,
                    cv_claimant_Name: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // These run commands
        cv_claimant_Birthdate: {
            label: "Birthdate (Birthdate: date)",
            group: "cv_people",
            command: "cv_claimant_Birthdate"
        },
        cv_claimant_Name: {
            label: "Full Name (Name: string)",
            group: "cv_people",
            command: "cv_claimant_Name"
        },
        // Another sub-menu
        cv_claim: {
            label: "Claim (claim)",
            group: "cv_claim",
            getItems: function() {
                return {
                    cv_claim_CreatedDate: CKEDITOR.TRISTATE_OFF,
                    cv_claim_Name: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // These run commands
        cv_claim_Name: {
            label: "Claim Number (Name: string)",
            group: "cv_claim",
            command: "cv_claim_Name"
        },
        cv_claim_CreatedDate: {
            label: "Created Date (CreatedDate: datetime)",
            group: "cv_claim",
            command: "cv_claim_CreatedDate"
        },
    });
}

// Add and configure the editor
function addEditor() {
    CKEDITOR.replaceAll('ckeditor');
    for (var name in CKEDITOR.instances) {
        var e = CKEDITOR.instances[name];
        e.config.height = 400;
    }
    CKEDITOR.on('instanceReady', addMergeFieldMenu);
}
addEditor();

</script>
Advertisements

Making one DataTable respond to search/length/page changes in another DataTable

DataTables can be applied to multiple tables in a page and sometimes the content in one table needs to be driven by the content in another table. For example, checkboxes in the first table on a page might act as a filter on later tables in a page. (The setup of such filtering is not covered here; the mechanism that can be used is custom filtering.)

But DataTables also supports a search mechanism that reduces the table rows to ones that match and a pagination mechanism where the number of rows shown or the page shown can be changed. By default, changes to those values in the first table will not cause the later tables to be re-filtered and re-drawn. So if you want the later tables to only correspond to the checked checkboxes that are visible in the first table, extra code has to be added.

The good news is that DataTables does generate events for the changes and so provides a convenient point to hook in extra code. The only complication is that it appears necessary to defer until after the current event processing has been completed by using setTimeout.

So assuming the tables are distinguished by classes firstMarker and laterMarker, this code will get the later tables re-drawn (and so re-filtered) to be consistent with the first table:

(function($) {
    $(document).ready(function() {

        var firstTable = $('table.firstMarker');
        firstTable.DataTable();

        var laterTables = $('table.laterMarker');
        var laterDataTables = [];
        laterTables.each(function() {
            laterDataTables.push($(this).DataTable());
        });

        var drawLaterDataTables = function() {
            setTimeout(function() {
                $.each(laterDataTables, function(index, value) {
                    value.draw();
                });
            }, 0);
        };
        firstTable.on('search.dt', drawLaterDataTables);
        firstTable.on('length.dt', drawLaterDataTables);
        firstTable.on('page.dt', drawLaterDataTables);

        // Filtering logic not shown here
    });
})(jQuery.noConflict());

Referencing one controller extension from another controller extension

A controller extension can add functionality to a standard controller. The controller extension coding pattern means an extension gets a reference to the standard controller and so can uses its methods:

public with sharing class Ext1 {
    public Ext1(ApexPages.StandardController sc) {
        ...
    }
}

But what if you have a page with two extensions:

<apex:page standardController="Contact" extensions="Ext1, Ext2">
    ...
</apex:page>

and you want to reference the methods of one extension from the other extension? No platform API is provided for that.

Here is some code that allows such cross-referencing. When the extensions are constructed, they are added (registered) into a singleton (static) instance. But a reference to that singleton is also made a field in each extension ensuring that the extension references are made part of the view state. So the cross-references are preserved across form posts etc.

The extensions look like this:

public with sharing class Ext1 {
    private Registry r;
    public Ext1(ApexPages.StandardController sc) {
        r = Registry.instance();
        r.add(Ext1.class, this);
        ...
    }
    private Ext2 getExt2() {
        return (Ext2) r.get(Ext2.class);
    }
}
public with sharing class Ext2 {
    private Registry r;
    public Ext2(ApexPages.StandardController sc) {
        r = Registry.instance();
        r.add(Ext2.class, this);
        ...
    }
    private Ext1 getExt1() {
        return (Ext1) r.get(Ext1.class);
    }
}

and the singleton is:

public class Registry {
    private static Registry instance;
    private Map<Type, Object> m = new Map<Type, Object>();
    // Set a view state field to this
    public static Registry instance() {
        if (instance == null) instance = new Registry();
        return instance;
    }
    // Singleton
    private Registry() {
    }
    public void add(Type key, Object value) {
        m.put(key, value);
    }
    public Object get(Type key) {
        return m.get(key);
    }
}

Adding JavaScript and jQuery to Visualforce pages

Visualforce is pretty tolerant of how custom JavaScript is added, but this post suggests a couple of patterns to use (where possible):

  • External JavaScript file references or local JavaScript is best placed at the end of the page rather than the beginning which looks a bit strange at first sight. The benefit is that the page content can then be rendered by the browser before the browser becomes blocked loading the external JavaScript file and then executing that file and the local Javascript. So a potential page load delay is avoided, with the JavaScript work being completed in the user’s “thinking time” as they first see the page content.
  • It is easy to unintentionally add references to Javascript’s global scope and so potentially interfere with other references in that scope. In JavaScript scope is delineated by functions (not by blocks – curly brackets have no impact on scope) so var declarations should always be used and should always be used within a function.

So when not using external libraries put this executed (via the ()) anonymous JavaScript function (to create a new scope) at the end of the page:

<apex:page ...>
    <apex:sectionHeader .../>
    <apex:pageBlock ...>
        ...
    </apex:pageBlock>

<script>
(function() {
    // All the custom JavaScript goes in here
    // Always use var
    var i = ...;
    ...
})();
</script>
</apex:page>

and when using jQuery put both the include and the executed anonymous JavaScript function at the end of the page:

<apex:page ...>
    <apex:sectionHeader .../>
    <apex:pageBlock ...>
        ...
    </apex:pageBlock>

<apex:includeScript value="{!URLFOR($Resource.jQueryZip, 'jquery.js')}"/>
<script>
(function($) {
    // All the custom JavaScript goes in here
    // Always use var
    var i = ...;
    // Use $ in here for jQuery
    var j = $('table.notes');
    ...
})(jQuery.noConflict());
</script>
</apex:page>

This allows $ to be used as the reference to jQuery within the function, while ensuring that whatever the symbol $ was set to before jQuery was included is restored via the noConflict call.

In praise of apex:inlineEditSupport

I recently had a requirement where one date value in a calculated table needed to be manually editable. While it would be possible to use an apex:inputField for all the values, using apex:inlineEditSupport seemed like a better approach because the need to edit is relatively rare. That allows the table to look uncluttered for the common case of no editing, yet still allows the values to be changed.

This is the result:

inlineedit

The point of this post is to highlight how easy this is to accomplish and to give a +1 to apex:inlineEditSupport in case anyone has been wary of using it in the several years it has been available…

All that was needed was to change this:

<apex:pageBlockTable value="{!payments}" var="p">
    ...
    <apex:column value="{!p.IssueDate__c}"/>
    ...
</apex:pageBloackTable>

to this:

<apex:pageBlockTable value="{!payments}" var="p">
    ...            
    <apex:column>
        <apex:facet name="header">
            <span class="inlineEditPencil">
                {!$ObjectType.Payment__c.fields.IssueDate__c.label}
            </span>
        </apex:facet>
        <apex:outputField value="{!p.IssueDate__c}">
            <apex:inlineEditSupport event="ondblclick"
                    showOnEdit="save, cancel"
                    hideOnEdit="submit, approve, send"
                    />
        </apex:outputField>
    </apex:column>
    ...
</apex:pageBloackTable>

and to add a couple of new command buttons (“save” and “cancel”) for the inline editing mode.

This CSS displays the pencil icon in the heading (to hint that the column is different):

<style type="text/css">
/* Fragile */
.inlineEditPencil {
    padding-right: 16px;
    background: url(/img/func_icons/util/pencil12.gif) no-repeat right 2px;
}
</style>

How to pass a large number of selections from a standard list view to a Visualforce page

Buttons can be defined and added to an object’s standard list view and these buttons can access the selected objects:

listview

If only a few items are selected then the IDs can be passed on to a Visualforce page like this (a “List Button” with “Display Checkboxes” checked that has behavior “Execute JavaScript” and content source “OnClickJavaScript”):

var ids = {!GETRECORDIDS($ObjectType.Contact)};
if (ids.length) {
    if (ids.length <= 100) {
        window.location = '/apex/Target?ids=' + ids.join(',');
    } else {
        alert('Select 100 or less');
    }
} else {
    alert('Select one or more Contacts');
}

The limit of 100 selected items is imposed because 2k characters is generally considered the longest URL that works safely everywhere, and a GET requires that each 15 character ID is appended to the URL together with a delimiter (to pass the values).

So what if you need to support more than 100 selected objects? (The standard list view UI does allow selections to be made on multiple pages and allows a single page to show up to 200 objects.) Using a POST instead of a GET where the ID values are part of the request body rather than the URL avoids the URL length problem. Here is how to do that:

var ids = {!GETRECORDIDS($ObjectType.Contact)};
if (ids.length) {
    var form = document.createElement("form");
    form.setAttribute("method", "POST");
    form.setAttribute("action", "https://c.na15.visual.force.com/apex/Target");
    var hiddenField = document.createElement("input");
    hiddenField.setAttribute("type", "hidden");
    hiddenField.setAttribute("name", "ids");
    hiddenField.setAttribute("value", ids.join(','));
    form.appendChild(hiddenField);
    document.body.appendChild(form);
    form.submit();
} else {
    alert('Select one or more Contacts');
}

This JavaScript creates a form and posts it to the server. An important part of making this work is that the full target URL needs to be specified as described in this Get POST data via visualforce page article otherwise the Apex controller can’t pickup the posted data via ApexPages.currentPage().getParameters(). An obscure trick to say the least.

This page:

<apex:page controller="Target">
    <h1>Target</h1>
    <apex:repeat value="{!ids}" var="id">
        <div>{!id}</div>
    </apex:repeat>
</apex:page>

and controller:

public with sharing class Target {
    public String[] ids {
        get {
            if (ids == null) {
                String s = ApexPages.currentPage().getParameters().get('ids');
                if (s != null) {
                    ids = s.split(',');
                } else {
                    ids = new String[] {};
                }
            }
            return ids;
        }
        private set;
    }
}

can be used to demonstrate that the IDs are passed correctly for both versions of the JavaScript button.

Serving AngularJS templates from static resources

An AngularJS app typically starts with an “index” page that loads the required JavaScript/CSS and acts as the container for the client-side processed content. The app operates by rendering various templates in response to user interactions into that container.

That “index” page is a good place to obtain information from the “Visualforce” world that can be passed to the “AngularJS” world, and so is best made a Visualforce page. (See Passing platform configuration to an AngularJS app.)

But what about the templates? Typically there are many of these. Should they also be Visualforce pages? At first sight it seems a reasonable thing to do as the templates are “partial pages”. And Visualforce pages have fixed URLs whereas static resources have URLs that include a timestamp making them harder to reference in JavaScript code such as a route provider. And if you use individual static resources per template (rather than a ZIP static resource containing all the templates) each template has its own timestamp.

But providing a clear separation has been made between server-side processing and client-side processing, no Visualforce capabilities are needed for the templates. And using Visualforce pages adds complexity such as requiring profiles to be updated. So how can the static resource timestamp value be handled if static resources are used instead?

The answer is surprisingly simple: it appears that using the current (JavaScript) timestamp is enough to get the latest version. So a $routeProvider templateUrl for a static resource called “xyz_partial” is simply:

templateUrl: '/resource/' + Date.now() + '/xyz_partial'

You can see this pattern applied in this (quite new) Salesforce AngularJS sample application created by Pat Patterson.

PS As David Esposito comments, where there are only a small number of resource references, it is arguably cleaner to not use this timestamp approach.