Tuesday, November 03, 2009

Zero ajax label help tooltips for Apex?

Always check out the original article at http://www.oraclequirks.com for latest comments, fixes and updates.

A couple of weeks ago i finally managed to complete a quick demo page showing a technique for displaying label tooltips without using Ajax or Javascript.
In practice, this means displaying the help text associated with the page item inside a sort of balloon that pops up whenever the mouse cursor hovers above the label link.
The floating label tooltip is rendered using CSS styles, which means that the whole thing is carried out on the client side, without the need of an extra round trip to the server to get that piece of information.
If we are talking of performance improvements, any technique that saves an additional request to the database should be considered positively.

This technique has been invented by Stuart Nicholls and i merely adapted it for use with Apex, with minor differences.
Stuart's site is very inspiring for anyone who wants to leverage the power of CSS stylesheets to achieve cool visual effects without resorting to the omnipresent javascript and i encourage everyone to have a look at it.

Basically the CSS technique consists in retrieving the help text of all the items involved upfront, format them using certain HTML tags that are matched by rules in the CSS stylesheet.
This at first may seem a contradiction, as i stated that the whole purpose of this was to save a request to the database, but if you compare this to a technique involving Ajax requests, it should be clear the advantage:
with the Ajax approach, a request is sent *every* time the cursor moves over a label link, no matter if the user already did it a few seconds or minutes before, while in the other case, the database request is made using a fast bulk collect statement retrieving all the involved labels in one shot, then it can be rendered as many times as necessary without accessing the db anymore, even if the user moves back and forth from other pages as the content is cached in session state.

Of course there are also a few limitations with this technique which can make it unsuitable for certain situations, for instance it can become unpractical for large texts or when the text contains links that you expect a user to click on. In these situations it makes sense to stick to the Ajax approach, and you could easily mix both on the same page as desired by choosing different label templates. The other major objection to this approach can be in the requirement of creating hidden items to hold the text. Of course it would be much better if we could have a substitution string for retrieving the help text at page rendering time, but i don't know if the Apex team is willing to include the code to support this in a future release.

Given the aforementioned prerequisites, here are the components to get it done in Apex (item names refer to the demo page hosted on apex.oracle.com:
-- before header process, conditional on (P11_ITEM1_HELP is null)
-- the process will run only the first time the page is loaded
-- thereafter values cached in session state will be used.
-- Each item with tooltip needs a hidden item to store the text
-- i.e. P11_ITEM1 is matched by P11_ITEM1_HELP.
declare
type vc2_4k_tab is table of varchar2(4000) index by binary_integer;
type vc2_255_tab is table of varchar2(255) index by binary_integer;
type vc2_coll is table of varchar2(4000) index by varchar2(255);
apex_item_names vc2_255_tab;
apex_item_help_text vc2_4k_tab;
apex_item_help_coll vc2_coll;
begin
select item_name, item_help_text
bulk collect into
apex_item_names, apex_item_help_text
from apex_application_page_items
where application_id = :APP_ID
and page_id = :APP_PAGE_ID
and item_label_template = 'Optional Label with Tooltip';

for i in 1..apex_item_names.count loop
apex_item_help_coll(apex_item_names(i)) := apex_item_help_text(i);
end loop;

:P11_ITEM1_HELP := apex_item_help_coll('P11_ITEM1');
:P11_ITEM2_HELP := apex_item_help_coll('P11_ITEM2');
end;


-- content of the CSS stylesheet (either inline or external)

a.t18tooltip {
text-decoration:none;
font-weight: bold;
}

a.t18tooltip cite {
display:none;
}

a.t18tooltip:hover {
cursor: help;
border:0;
position:relative;
z-index:500;
text-decoration:none;
}

a.t18tooltip:hover cite {
cursor: help;
display:block;
position:absolute;
top:20px;
left:-25px;
padding:5px;
font-weight:normal;
font-style:normal;
color:#000;
border:1px solid #888;
background:#ffc;
width:150px;
text-align: left;
}

a.t18tooltip:hover cite em {
position:absolute;
left:20px;
top:-6px;
width:11px;
height:6px;
background:#fff url(#WORKSPACE_IMAGES#tooltip.gif) 0 0;
display:block;
font-size:1px;
}

<a class="t18tooltip" href="javascript:popupFieldHelp('#CURRENT_ITEM_ID#','&SESSION.')">
Test Item 1<cite><em></em>&P11_ITEM1_HELP.</cite>
</a>


Please note that #WORKSPACE_IMAGES# in the case of Apex with EPG (Embedded PL/SQL Gateway), will generate one db roundtrip. As of Apex 3.1 images in the repository are cached by the browser, however there is still one request to retrieve the E-TAG attribute and compare it with the cached version. If the HTTP server is not EPG, then there won't be any db requests, just a web server request. This tiny image is required to show the nice "spike" pointing to the label, but it can be omitted or perhaps you can dare to implement the second method for rendering CSS tooltips slightly more elaborated to achieve the effects by means of border graphics only.

Updated January 26, 2010
After further thinking, i made some improvements to the technique shown above.
First of all i found out a sort of hack that allows me to put all the required HTML code inside the label template, thereby simplifying the whole thing. No more HTML tags inside the label text and no more explicit reference to the hidden item holding the help text:

-- this code goes into the before label template
<label for="#CURRENT_ITEM_NAME#" tabindex="999">
<a class="t18tooltip" href="" tabindex="999">


-- this code goes into the after label template
<cite><em></em>&#CURRENT_ITEM_NAME#_HELP.</cite><
</a>
</label>


The trick consists in building a dynamic substitutions by means of #CURRENT_ITEM_NAME#, appending the suffix _HELP. Apex processes the label template substitution variable first, then it uses the resulting string for the subsequent substitution of the resulting page item (at  a later stage of the label rendering), on the assumption that the help text is held by a hidden page item whose name is made up of the item name plus the _HELP suffix.

As i said this gives me two advantages compared to the initial method:
  1. i do not need to use HTML tags inside the label text
  2. i do not need to manually specify the hidden item name every time
Cool!

Secondly, i came up with a generalized on-demand process that i invoke as a before header process in the relevant pages (the pages containing items with tooltips). This code is completely generic so i don't even need to bother specifying the items names one by one, so this is another improvement over the initial version.


procedure init_page_tooltips (
p_app_id in pls_integer,
p_page_id in pls_integer,
p_temp_name in varchar2
as
type vc2_4k_tab is table of varchar2(4000) index by binary_integer;
type vc2_255_tab is table of varchar2(255) index by binary_integer;
apex_item_names vc2_255_tab;
apex_item_help_text vc2_4k_tab;
begin
select item_name, item_help_text bulk collect
into apex_item_names, apex_item_help_text
from apex_application_page_items
where application_id = p_app_id
and page_id = p_page_id
and item_label_template = p_temp_name;

for i in 1..apex_item_names.count loop
APEX_UTIL.SET_SESSION_STATE(apex_item_names(i)||'_HELP', apex_item_help_text(i));
end loop;
end;


Note that APEX_UTIL.SET_SESSION_STATE uses a rather uncommon approach when it comes to report errors.
Undefined items will not generate an error at the time of calling the API (that is inside the loop), but the Apex error page will be displayed later. In other words we cannot trap the error inside our procedure because this API sets some status variable inside the Apex engine. As a consequence, be aware that a missing help element will "break" the page.
I hoped i could work around this, but, up to date, i could not find how. This is not a big problem though because we do want to create such elements anyway, so the workaround is to change the label template to a different value until the help item holder has been added to the page.

Another optimization consists in specifying a non default value for the aforementioned on-load before header process: instead of keeping the default setting of "Once per page visit" and then making the process conditional, as illustrated in the article above, i chose "Once per session or when Reset", so it runs only the first time you load a page.

Last, be sure to remove any nowrap or nowrap="nowrap" from the HTML Table Cell Attributes in the Label section of the page item (not the help container), otherwise the tooltip will not fit in the box.

See more articles about Oracle Application Express or download tools and utilities.

3 comments:

Martin D'Souza said...

Hi Flavio,

Great post! I noticed that you have to create extra items for each help text. Here's a technique where you don't need to create the hidden help text items: http://apex-smb.blogspot.com/2009/09/tooltip-help-in-apex-alternative-to.html

The only catch is the technique uses JavaScript...

Martin

Byte64 said...

Hi Martin,
i saw the the script, it may come in handy for a "mixed case".

Perhaps one could use the APEX_UTIL.HIDDEN API to automatically create the hidden items basing on the name of the items associated with the tooltip template, a sort of variation of the script i supplied to seed the help text.

I didn't test this possibility, may be it's worth trying later.

Thank you!
Flavio

Mamun said...

nice

http://oracletheworld.blogspot.com/

yes you can!

Two great ways to help us out with a minimal effort. Click on the Google Plus +1 button above or...
We appreciate your support!

latest articles