PDA

View Full Version : Implementing a context-sensitive help



timo.nuros
19 Mar 2009, 8:48 AM
Dear Ext-Users,

I'm currently implementing a context-sensitive help, however, I'm a littlebit stuck.

The process should be similar to the known function in Windows, where you click the little "help" icon in the window, then click on the element you want to get help for. ExtJS already provides a nice tool icon.

I have 2 implementation ideas:

1.) Extend the onClick-Handler to intercept all click events as soon as the context-sensitive help mode has been activated, and have Ext.util.Observer handle the displaying of the help message. Biggest advantage is that there's only one code location to override. The problem with that approach is that not each target recieves click-events, for example, the Ext.form.Textfield.

2.) Overlay the page and recieve the click event from that overlay. Then find the element at the click's coordinates, and call the help display from there. However, I did not find any function which returns the element(s) for a given mouse position.

Any hints/pointers on how to solve such a thing?

Thanks and best regards,
Timo

Animal
19 Mar 2009, 9:10 AM
If you have a list of Components which have help available, then you can use the masking option.

The click handler can loop through the Components, seeing if the Ext.lib.Point() represented by the event's coordinates are within the Ext.lib.Region of the Component.

If so, break the loop, you know which Component has been clicked over.

timo.nuros
19 Mar 2009, 9:15 PM
Thanks for the pointer animal, I used the following schema now:

- Create a layer over the viewport, which recieves a click event
- On click, iterate through all components via Ext.ComponentMgr.all.each() and check if the target is inside the region
- If it is inside the region, see if it has a property "helpId". If yes, display the help in a rich tooltip or anything else :)

The whole code is less than 50 lines. Thanks alot for your help!

Animal
19 Mar 2009, 10:14 PM
Hey, good work!

It's nice to see someone understand the principle, investigate it and run with it instead of responding with a request for "an example"=D>=D>

timo.nuros
19 Mar 2009, 11:43 PM
I hear you :)

The whole thing proceeded pretty smoth, if you want to have a look, here are some screenshots (unfortunately the gnome screen grabber doesn't save the mouse cursor, but I think you get the point):

http://www.timohummel.com/temp/screens/help1.png
http://www.timohummel.com/temp/screens/help3.png
http://www.timohummel.com/temp/screens/help4.png

The actual help texts are maintained in a MediaWiki, and are loaded via AJAJ. A developer only needs to set the object attribute "helpID", which corresponds to the page in the wiki. I plan to implement a "Read more..." link later, where users can share their experience and contribute comments via the Wiki. The whole thing is actually so simple, I wonder why nobody did it before.

watrboy00
21 Mar 2009, 4:06 PM
That's a pretty useful feature I would say. I try to build in tooltips to as many of the components as I possibly can but I think this takes it one step further.

Not to mention linking to a wiki that can be updated on the fly generally leads to documentation that easily changes with the times.

Animal
21 Mar 2009, 10:30 PM
This singleton class might help.

Just do



Ext.ux.ComponentTracker.findComponent(handleComponentClick, optionalScope);


That does a one-time masking and click listening operation, and calls your method in the requested (optional) scope, passing the Component when a Component is clicked on.

It drills down to the lowest Component level at the clicked coordinates.



/**
* @class Ext.ux.ComponentTracker
* @singleton
* <p>This object offers mouse-based location of Components in the document. Components can be clicked upon
* which will fire the {@link #click} event, or, they may be mouseovered which will fire the {@link #mouseenter} event.</p>
* <p>To activate click detection, use the {@link #findComponent} method.</p>
* <p>To activate mouseover detection, use the {@link #trackMouseOver} method.</p>
*/
/**
* @event click
* Fires when the mouse is clicked on a Component. Fires on the lowest component
* in the hierarchy at the click point.
* @param {Ext.Component} c The Component clicked on.
* @param {Ext.EventObject} The click event.
*/
/**
* @event mouseenter
* Fires when the mouse enters a Component.
* @param {Ext.Component} c The Component the mouse moved into.
* @param {Ext.EventObject} The mousemove event.
*/
/**
* @event mouseleave
* Fires when the mouse exits a Component <b>including when entering a child Component</b>.
* @param {Ext.Component} c The Component the mouse moved out of.
* @param {Ext.EventObject} The mousemove event.
*/
Ext.ux.ComponentTracker = Ext.apply(new Ext.util.Observable(), {
// private
// Masks the document body with a (nearly) transparent masking element.
maskBody: function() {
this.mask = Ext.getBody().mask(null, '');
Ext.getBody().removeClass('x-masked');
this.mask.setHeight(Ext.lib.Dom.getDocumentHeight());
this.mask.setStyle({
'background-color': '#fff',
opacity: 0.01
});
},

/**
* <p>Activates detection of clicks in Components in the document. When activated, a click on a Component
* fires the {@link #click} event.</p>
* <p>By default, when a Component is clicked upon, click detection is deactivated.</p>
* @param fn {Function} A function to call when a Component is clicked on.
* @param scope {Object} (Optional) The scope (<tt>this</tt> reference) in which the function is executed.
* @param single {Boolean} (Optional, defaults to <b><tt>true</tt></b>) Deactivate detection when the document is clicked upon.
*/
findComponent: function(fn, scope, single) {
this.stopFind();
var me = this, wfn;
me.singleClick = (single !== false);
me.maskBody();
me.mask.on({
click: me.onMaskClick,
scope: me,
single: me.singleClick
});
if (fn) {
me.clickFn = me.singleClick ? function() {
me.un('click', me.clickFn);
fn.apply(scope || window, arguments);
} : fn;
me.on('click', me.clickFn);
}
},

/**
* Cancels listening for clicks on Components if the <tt>single</tt> parameter was passed as <tt>false</tt>
* to {@link #findComponent}
*/
stopFind: function() {
var me = this;
if (me.mask) {
Ext.getBody().unmask();
me.mask.un('click', me.onMaskClick, me);
me.un('click', me.clickFn);
delete (me.mask);
}
},

/**
* Activates detection of mouse events in Components in the document. When activated, moving the
* mouse into a Component fires the {@link #mousenter} event, and moving the mouse out of a Component
* (or into a child Component) fires the {@link #mouseleave} event..
* @param enterFn {Function} A function to call when a Component is entered.
* @param enterFn {Function} A function to call when a Component is exited.
* @param scope {Object} (Optional) The scope (<tt>this</tt> reference) in which the functions are executed.
*/
trackMouseOver: function(enterFn, leaveFn, scope) {
var me = this;
me.stopTracking();
Ext.getBody().on({
mousemove: me.onBodyMouseMove,
mouseleave: me.onBodyMouseLeave,
scope: me
});
if (enterFn && leaveFn) {
me.enterFn = enterFn;
me.leaveFn = leaveFn;
me.trackScope = scope || me;
me.on({
mouseenter: enterFn,
mouseleave: leaveFn,
scope: me.trackScope
});
}
},

/**
* Deactivates detection of mouse events in Components in the document.
*/
stopTracking: function() {
var me = this;
delete me.currentOverComponent;
Ext.getBody().un('mousemove', me.onBodyMouseMove, me);
Ext.getBody().un('mouseleave', me.onBodyMouseLeave, me);
if (me.enterFn && me.leaveFn) {
me.un('mouseenter', me.enterFn, me.trackScope);
me.un('mouseleave', me.leaveFn, me.trackScope);
}
},

onBodyMouseMove: function(e) {
var me = this;
if (c = me.getComponentFromEvent(e)) {
if (c !== me.currentOverComponent) {
if (me.currentOverComponent) {
me.onComponentLeave(e);
}
me.currentOverComponent = c;
me.fireEvent("mouseenter", c, e);
}
} else if (me.currentOverComponent) {
me.onComponentLeave(e);
}
},

onBodyMouseLeave: function(e) {
if (this.currentOverComponent) {
this.fireEvent("mouseleave", this.currentOverComponent, e);
}
},

onComponentLeave: function(e) {
if (this.currentOverComponent) {
this.fireEvent("mouseleave", this.currentOverComponent, e);
delete this.currentOverComponent;
}
},

onMaskClick: function(e) {
var me = this, c;
if (c = this.getComponentFromEvent(e)) {
me.fireEvent("click", c, e);
} else if (this.singleClick) {
me.un('click', me.clickFn);
Ext.getBody().unmask();
}
},

/**
* Returns the lowest level Component at the point encapsulated by the passed <b>mouse</b> event.
* @param {Ext.EventObject} e The <b>mouse</b> event for which to find the associated Component.
* @return {Ext.Component} The Component at the point encapsulated by the passed <b>mouse</b> event.
*/
getComponentFromEvent: function(e) {
return this.getComponentFromPoint(e.getPoint());
},

/**
* Returns the lowest level Component at the specified point.
* @param {Ext.lib.Point/Number} p The Point at which to find the associated Component, or the X coordinate of the point.
* @param {Number} y If passing coordinates, the Y coordinate of the point.
* @return {Ext.Component} The Component at the specified point.
*/
getComponentFromPoint: function(p, y) {
if (arguments.length == 2) {
p = new Ext.lib.Point(p, y);
}
var i, c, el, items = Ext.ComponentMgr.all.items, l = items.length;
for (i = 0; i < l; i++) {
c = items[i];
if ((!c.ownerCt) && (el = (c.getPositionEl ? c.getPositionEl() : c.getEl()))) {
if (el.getRegion().contains(p)) {
if (c instanceof Ext.Container) {
c = this.getDescendantAtPoint(c, p);
}
return c;
}
}
}
},

getDescendantAtPoint: function(c, p) {
var el, cc, items;
if ((el = c.getEl()) && el.getRegion().contains(p)) {

// The Component contains the point directly.
if (!(c instanceof Ext.Container)) {
return c;
}

// Collect all Components which may be inside this Component
items = c.items ? c.items.items : [];
if (c.getTopToolbar && (cc = c.getTopToolbar())) {
items = items.concat(cc.items.items);
}
if (c.getBottomToolbar && (cc = c.getBottomToolbar())) {
items = items.concat(cc.items.items);
}
if (c.buttons) {
items = items.concat(c.buttons);
}

// If a child contains the Point, return it
for (var i = 0, l = items.length; i < l; i++) {
if (cc = this.getDescendantAtPoint(items[i], p)) {
return cc;
}
}

// Not over any of the children, just over the Component
return c;
}
}
});

timo.nuros
21 Mar 2009, 11:19 PM
Yes, a Wiki with special addons makes context-sensitive help a real help and not only a "reference", where users can share their experience for a specific function and exchange tips.

My implementation is a bit different from Animal's and not documented yet:



org.jerrymouse.gui.contexthelp.handler = function () {
Ext.setContextHelp();
}

Ext.setContextHelp = function () {
this.contextHelp = true;

var layer = new Ext.Layer();

var size = Ext.getCmp("jm-main-window").getSize();

layer.setSize(size);
layer.setStyle("cursor", "help");
layer.on("click", function (a) {
Ext.unsetContextHelp();
this.remove();
Ext.displayHelpForCoordinates(a.xy);


}.createDelegate(layer));
layer.show();
}

Ext.unsetContextHelp = function () {
this.contextHelp = false;
}

Ext.isContextHelp = function () {
return this.contextHelp;
}

Ext.displayHelpForCoordinates = function (xy) {
var point = new Ext.lib.Point(xy[0], xy[1]);

Ext.ComponentMgr.all.each( function (item) {
if (item && item.getEl && item.getEl() && item.getEl().getRegion) {
if (item.getEl().getRegion().contains(point)) {
if (item.initialConfig.helpId) {

if (Ext.helpTip) {
Ext.helpTip.destroy();
}

var call = new org.jerrymouse.service.Call(
new org.jerrymouse.lang.String("org.jerrymouse.system.help.HelpManager"),
new org.jerrymouse.lang.String("getHelp"));

call.setParameter("helpId", new org.jerrymouse.lang.String(item.initialConfig.helpId));
call.setLoadMessage('$[org.jerrymouse.gui.help.loading_message]');
call.setAnonymous(true);
call.setHandler(function (obj) {
Ext.helpTip = new Ext.ToolTip({
html: obj.getItem("content").toString(),
closable: true,
autoHide: false,
title: obj.getItem("title").toString(),
draggable: true,
targetXY: xy
});

Ext.helpTip.showBy(item.getEl(), 'tl-tr');
});


call.doCall();
return false;
}
}
}
return true;
});
}


You need to replace the custom AJAX-call (org.jerrymouse.service.Call) with your own implementation. You just have to add the property "helpId" to the components which should have a help tooltip displayed.

Lateron, the help tooltips just display the first paragraph from the wiki and offer a "read more" link, which, when clicked, displays the full wiki article with user comments.

Animal
21 Mar 2009, 11:30 PM
You could use the Ext.ux.ComponentTracker.getComponentFromEvent() method in your code.

I think it needs to do what I do because I don't know about the ordering of the events in Ext.ComponentMgr's Collection.

If you go through them sequentially, and just return the first Component which contains the Point, then you might get the FormPanel instead of the Field, because that was probably created first, and will probably be found before the Field.

This is why I have that extra stage of drilling down to find the lowest descendant which also contains the Point.

But the code is quite compact, and performs well.

timo.nuros
21 Mar 2009, 11:32 PM
Yes, thats a good point. I will rewrite that part later, I currently concentrate on getting the framework running. Thanks for the pointer!

zipi
23 Mar 2009, 1:00 AM
hello
maybe you could share the code with us? i want to do something like a checkbox on bottom of the page. when you check it the app changes to "Help Mode", where a lot of tooltips pop up on every function on the page, and the purpose of each function is described there.
thanks in advance!

timo.nuros
23 Mar 2009, 1:01 AM
The code was already posted above. But I doubt it will be helpful; you need to adjust it to your own needs.

With best regards,
Timo

foxmarco
26 Oct 2010, 5:31 AM
Some one could tell me how to use this component?? (Ext.ux.ComponentTracker) I suppose via Ext.ux.ComponentTracker i will active a function like open tooltip when click on a component area of my document... is it right?

An exmple of use?