Monday, 4 April 2011

af:table – Restoring the basic browser copy functionality

One of the first things any computing user learns is how to select text with the mouse or keyboard, then copy & paste the selected text. This simple functionality is supported in most applications including browsers, making it a very familiar facility to all users.

There are specific scenarios under certain browsers with the af:table component in ADF Faces RC 11.1.1.4.0 (also verified in 11.1.1.2.0) where the ability to select and copy text is absent.

This post identifies the specific scenarios, as well as a potential workaround for Internet Explorer (only).

The Issue

The following picture from Internet Explorer 7 (also replicated in IE8) shows a simple af:table without its selectionListener and rowSelection properties set.

If you look closely you can see with my mouse I've selected and highlighted the "Hong" in "HongKong".

While I can't demonstrate it in a screenshot, if you were to right click on the selected text you would see IE's standard right mouse menu with options such as Copy.

Here we can see similar functionality in Firefox 3.6, with the "Kingdom" in "United Kingdom" selected:

And finally Chrome 10, the "Can" in "Canada" selected:

(Unfortunately I don't have access to Safari to test its behaviour)

In all of these browsers it would be a reasonable requirement that the user would want to be able to highlight text and copy it to some other application running on their desktop.

Let's revisit the same af:table within each browser, this time with the selectionListener and rowSelection properties set.

The following picture shows IE7 (also confirmed in IE8) where you can see the effects of the selectionListener and rowSelection properties, highlighting the selected row. But what you can't do with your mouse is select any text. This disables the user from copying any text from the table:

Yet oddly in Firefox 3.6 the user can do both:

And in Chrome 10 they can too:

The behaviour or lack of it in Internet Explorer is certainly undesirable, even more so as most corporate environments will choose IE as their preferred browser. However via bug 9830307 Oracle has noted that this is in fact by design, and Oracle is planning to remove the functionality from the other browsers too.

Solution

The following technique demonstrates a solution for Internet Explorer displaying an af:table with its selectionListener and rowSelector properties set.

The technique requires us to create our own Copy contextMenu option for the table, and then via a combination of JavaScript, af:clientListener and af:clientAttribute tags, we'll copy the user selected table cell's data to the clipboard.

Starting out this is the code for the page containing the af:table before any modifications:

xmlns:h="http://java.sun.com/jsf/html" xmlns:af="http://xmlns.oracle.com/adf/faces/rich">





rows="#{bindings.CountriesView1.rangeSize}"
emptyText="#{bindings.CountriesView1.viewable ? 'No data to display.' : 'Access Denied.'}"
fetchSize="#{bindings.CountriesView1.rangeSize}" rowBandingInterval="0"
selectedRowKeys="#{bindings.CountriesView1.collectionModel.selectedRow}"
selectionListener="#{bindings.CountriesView1.collectionModel.makeCurrent}" rowSelection="single"
id="t1" columnStretching="last">
headerText="#{bindings.CountriesView1.hints.CountryId.label}" id="c1">


headerText="#{bindings.CountriesView1.hints.CountryName.label}" id="c2">






We then extend the af:table with a contextMenu facet, which includes an af:clientListener:








For each column we wish to provide the copy functionality, we extend each by including an af:clientListener tag and af:clientAttribute tag as follows:
                     headerText="#{bindings.CountriesView1.hints.CountryId.label}" id="c1">




....note the use of the af:clientListener and af:clientAttribute tags *within* the af:outputText. Also note how the af:clientAttribute's value EL expression has been changed to match that of the parent af:outputText value.

From the code above, each af:clientListener makes a call to separate JavaScript functions captureTableFieldName() and copyMenu. We provide these in JavaScript attached to the page :

var globalLastVisitedField = null;

/*
* Given the user clicking on a field in a table, captures the field name to be later used by the copyMenu
* function
*/
function captureTableFieldName() {
return function (evt) {
evt.cancel();
globalLastVisitedField = evt.getSource();
}
}
/*
* Function referenced from the clientListener on the copy menu option
*/
function copyMenu(evt) {
// Copy the last visited field to the clipboard
if (globalLastVisitedField == null) {
alert("copyMenu() Error: No field could be identified to be in focus");
}
else if (navigator.appName != "Microsoft Internet Explorer") {
alert("Copy function is only supported in Microsoft Internet Explorer");
}
else {
var txt = globalLastVisitedField.getProperty("ItemValue");
window.clipboardData.setData('Text', "" + txt);
}
evt.cancel();
}
The complete code for the page is as follows:

xmlns:h="http://java.sun.com/jsf/html" xmlns:af="http://xmlns.oracle.com/adf/faces/rich">




var globalLastVisitedField = null;

/*
* Given the user clicking on a field in a table, captures the field name to be later used by the copyMenu
* function
*/
function captureTableFieldName() {
return function (evt) {
evt.cancel();
globalLastVisitedField = evt.getSource();
}
}
/*
* Function referenced from the clientListener on the copy menu option
*/
function copyMenu(evt) {
// Copy the last visited field to the clipboard
if (globalLastVisitedField == null) {
alert("copyMenu() Error: No field could be identified to be in focus");
}
else if (navigator.appName != "Microsoft Internet Explorer") {
alert("Copy function is only supported in Microsoft Internet Explorer");
}
else {
var txt = globalLastVisitedField.getProperty("ItemValue");
window.clipboardData.setData('Text', "" + txt);
}
evt.cancel();
}



rows="#{bindings.CountriesView1.rangeSize}"
emptyText="#{bindings.CountriesView1.viewable ? 'No data to display.' : 'Access Denied.'}"
fetchSize="#{bindings.CountriesView1.rangeSize}" rowBandingInterval="0"
selectedRowKeys="#{bindings.CountriesView1.collectionModel.selectedRow}"
selectionListener="#{bindings.CountriesView1.collectionModel.makeCurrent}" rowSelection="single"
id="t1" columnStretching="last">









headerText="#{bindings.CountriesView1.hints.CountryId.label}" id="c1">





headerText="#{bindings.CountriesView1.hints.CountryName.label}" id="c2">









How The Solution Works

When the user wants to invoke the Copy function, there's effectively two actions. First they right click the individual af:outputText rendered in a cell of the table. Second after the contextMenu appears as a result of the right click, the user then left clicks on the Copy af:commandMenuItem. The action of selecting the af:commandMenuItem obscures the table cell clicked. As such the two distinct operations require we handle them separately:

1) On the first right click we capture the name of the field. This is what the af:clientListener within the af:outputText field does, by calling captureTableFieldName() storing the field name as a JavaScript global to be retrieved later.

2) On the left click on the Copy menu option that appears in the contextMenu, the af:clientListener within the af:commandMenuItem calls the copyMenu JavaScript function.

The copyMenu function armed with the field name captured and stored in the JavaScript global from step 1, retrieves the relating "ItemValue" property which is the value from the af:clientAttribute tag within the af:outputText. It then copies the relating value to the Browser's clipboard.

Note the call to window.clipboardData.setData. This is the limiting code that makes this solution only useful in Internet Explorer. From research there isn't a simple solution available in other browsers. As such in this example we simply alert the user the functionality is only supported in IE.

Limitations

From a design time point of view, having to add the af:clientListener and af:clientAttribute to each column is certainly a pain. If anybody can think of a simpler solution your comments would be appreciated.

From a runtime point of view, as already explained the workaround is specific to Internet Explorer.

Though we haven't undertaken significant amounts of testing we've also found limitations in applying this to check boxes and date fields. In the case of check boxes we can't seem to derive the underlying value, and with the date field we sometimes get the date and time in the wrong format. There's probably solves for both of these issues but beyond the general technique described here.

As usual developers adopting this code should be careful to test in their own environments as it hasn't been rigorously tested in a production environment.

Credit

Thanks to Penny Cookson from SAGE Computing Services for the original solution.

No comments: