Handling the context menu with a custom Atlas Behavior
One of the goals of Atlas is making easier to build DHTML-enabled controls, while helping to easily develop web sites and web applications by providing features like OOP patterns, components, bindings, transformers and more.
For example, Atlas controls can have behaviors, where a Behavior is a component used to dynamically add/remove client side functionality (mainly DHTML). Since Atlas components expose a behaviors collection, their client side capabilities can be defined and controlled programmatically by adding/removing behaviors from it. Examples of behaviors are the PopupBehavior that shows/hides a control, the FloatingBehavior that transforms a control into a floating control, the AutoCompleteBehavior that helps to fill text fields from a list of suggestions.
In this article we'll see how to code a simple Behavior (called ContextMenuBehavior) that allows to associate a context menu to a control by setting a contextMenuID property. When the user right-clicks on a control with a ContextMenuBehavior, a context menu is showed and a contextMenuShow event is raised.
Note: this Behavior works in browsers that support the oncontextmenu event, like for example IE6 and FireFox1.5.
Examining the code (you will find it at the end of this post), we'll overview some OOP patterns introduced by the Atlas framework, including namespaces, inheritance and other features like events, getters/setters, declarative usage in XML script. When dealing with OOP patterns, I will refer to Javascript's typed constructors as classes, and to variables and functions declared inside the class as fields and methods.
To be able to use the features provided by the Atlas framework, a custom class must implement one or more interfaces defined in the framework, or inherit from one of the base classes provided (tipically Sys.Component or Sys.UI.Control). The Sys.Component class provides the basic Atlas functionality while Web.UI.Control is the base class for all the UI Controls (wrappers for DOM elements like TextBox or Button, or composite controls).
The ContextMenuBehavior class that we are going to implement inherits from the Sys.UI.Behavior class (that is the base class for all behaviors), which in turn inherits from the Web.Component class.
Overview of the code
First of all, we have to register the namespaces that we intend to use. In this case, we register the namespace AtlasNotes.UI that will be the namespace for our class. Type.registerNamespace('AtlasNotes.UI');
The namespace registration, done with the call to registerNamespace(), is followed by the declaration of our class:
AtlasNotes.UI.ContextMenuBehavior = function() {
AtlasNotes.UI.ContextMenuBehavior.initializeBase(this);
...
where the first statement is a call to initializeBase(), that allows to properly setup instances within the Atlas framework.
Then, I've declared some fields supposed to have private scope (Javascript doesn't support access modifiers, so it is a convention to prefix with an underscore fields or methods with private scope). var _contextMenuHandler;
var _mousedownHandler;
var _contextMenuID;
var _contextMenu;
var _popupBehavior;
After private fields declaration, some public properties are exposed. Public properties are getters or setters in the Atlas framework. There is a convention that must be followed when naming public properties: a setter must have the prefix set_, while a getter must have the prefix get_. This ensures that our Behavior will work properly with declarative script or bindings.
I've also declared an event to be raised by the ContextMenuBehavior. An event is created with the createEvent() method: this.contextMenuShow = this.createEvent();
The event contextMenuShow is raised when the context menu is made visible after the user's right-click. Events in the Atlas framework behave in a manner similar to events in the .NET framework. They are handled by declaring an event handler (a function) for that particular event. When the event is raised, if an event handler is found, it is called with two arguments: a reference to the object that raised the event (the "sender") and a state object (the "event arguments"). For example, the following function acts as an event handler for the contextMenuShow event: function onContextMenuShow(sender, e) {
// Get the source control.
var src = sender.control.element;
}
Two important methods are those related to the lifecycle of our object. We can partecipate in initialization and disposing of the object, by overriding the initialize() and dispose() methods of our base class (Sys.UI.Behavior). In Javascript, we can "override" a method by declaring a function with the same signature. Moreover, the Atlas framework allows to call the method implementation of the base class, by invoking the callBaseMethod(), passing a reference to the current instance and the name of the base method to call (to be able to call the implementation of the base class, the corresponding method must have been registered in the base class with a call to registerBaseMethod()). In the code below, we are overriding the initialize()/dispose() methods, calling their base implementations and then adding custom code: this.initialize = function() {
AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'initialize');
...
this.dispose = function() {
AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'dispose');
...
In the initialize() method, I've setup handlers for the oncontextmenu and onmousedown events of the document DOM element. In these event handlers, the context menu is made visible, or hided if one left-clicks outside it. The event handlers are the mousedownHandler() and contextmenuHandler() methods defined in the class.
For example, look at this statement in the mousedownHandler(): var bounds = Sys.UI.Control.getBounds(_contextMenu.element);
There I invoked the getBounds() method of the Sys.UI.Control class. This method (you can think of it as a "static" method) returns the bounds of a DOM element in an object with four properties: x, y, width, height). I've used these properties to test if the user clicked outside the menu region. In the initialize() method I've also upgraded the context menu element to an Atlas control and added a PopupBehavior to it: _contextMenu = new Sys.UI.Control($(_contextMenuID));
_popupBehavior = new Sys.UI.PopupBehavior();
_popupBehavior.set_parentElement(this.control.element);
_contextMenu.get_behaviors().add(_popupBehavior);
_popupBehavior.initialize();
_contextMenu.initialize();
In the dispose() method, event handlers are detached from the corresponding DOM events and set to null for garbage collection.
An important method inherited from our base class is getDescriptor(). This method is defined in the Sys.Component class and allows to add support for usage of our Behavior in declarative XML script (the Atlas markup). To do that, we have to override the getDescriptor() method and add properties, methods and events to be mapped to XML tags and attributes: this.getDescriptor = function() {
var td = AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'getDescriptor');
td.addProperty('contextMenuID', String);
td.addEvent('contextMenuShow', true);
return td;
}
The getDescriptor method has been overridden above. We first call the base class implementation, that returns a reference to the type descriptor (td) and then add the property contextMenuID and the event contextMenuShow. Now, we can use the ContextMenuBehavior in the Atlas markup: <contextMenuBehavior contextMenuID="contextMenu1"
contextMenuShow="onContextMenu" />
At the end of the code, after the class declaration, we find two other important statements: AtlasNotes.UI.ContextMenuBehavior.registerClass('AtlasNotes.UI.ContextMenuBehavior', Sys.UI.Behavior);
Sys.TypeDescriptor.addType('script', 'contextMenuBehavior', AtlasNotes.UI.ContextMenuBehavior);
With the first statement, we make our class inherit from the Sys.UI.Behavior class. The second argument passed to the registerClass() method is the base type (Sys.UI.Behavior). If our class implements any interfaces, we have to pass their names as arguments after the base type.
Finally, with the call to addType() in the last statement, we register the ContextMenuBehavior to be recognized as a valid tag when used in XML script.
Summary
Behaviors are classes that inherit from the base Sys.UI.Behavior class. Basic steps to build a custom type within the Atlas framework are
- register the needed namespaces using the Type.registerNamespace() method;
- declare the class as a typed constructor using its full name (namespace.className);
- call the initializeBase() method of the base class to let the Atlas framework properly setup the instance;
- define fields, properties (using the getter/setter convention), events (using the createEvent() method), methods for the class;
- override the initialize()/dispose() methods of the base class to partecipate in the instance's lifecycle;
- override the getDescriptor() method of the base class to add support for use in XML script;
- register the class as a derived class calling the registerClass() method;
- register the class to be recognized as a valid element in XML script by invoking the Sys.TypeDescriptor.addType() method.
ContextMenuBehavior.js Type.registerNamespace('AtlasNotes.UI');
AtlasNotes.UI.ContextMenuBehavior = function() {
AtlasNotes.UI.ContextMenuBehavior.initializeBase(this);
// Private fields.
var _contextMenuHandler;
var _mousedownHandler;
var _contextMenuID;
var _contextMenu;
var _popupBehavior;
// Properties.
this.get_contextMenuID = function() {
return _contextMenuID;
}
this.set_contextMenuID = function(value) {
if(value != _contextMenuID) {
_contextMenuID = value;
this.raisePropertyChanged('contextMenuID');
}
}
// Events.
this.contextMenuShow = this.createEvent();
// Initialize / Dispose
this.initialize = function() {
AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'initialize');
_contextMenu = new Sys.UI.Control($(_contextMenuID));
_popupBehavior = new Sys.UI.PopupBehavior();
_popupBehavior.set_parentElement(this.control.element);
_contextMenu.get_behaviors().add(_popupBehavior);
_popupBehavior.initialize();
_contextMenu.initialize();
_contextMenuHandler = Function.createDelegate(this, contextMenuHandler);
document.attachEvent('oncontextmenu', _contextMenuHandler);
_mousedownHandler = Function.createDelegate(this, mousedownHandler);
document.attachEvent('onmousedown', _mousedownHandler);
}
this.dispose = function() {
AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'dispose');
document.detachEvent('oncontextmenu', _contextMenuHandler);
document.detachEvent('onmousedown', _mousedownHandler);
_contextMenuHandler = null;
_mousedownHandler = null;
}
// Handlers.
function contextMenuHandler() {
var evt = window.event;
var target = evt.srcElement;
if(target == this.control.element) {
_popupBehavior.set_x(evt.offsetX);
_popupBehavior.set_y(evt.offsetY);
_popupBehavior.show();
this.contextMenuShow.invoke(this, Sys.EventArgs.Empty);
evt.returnValue = false;
}
}
function mousedownHandler() {
var x = window.event.clientX;
var y = window.event.clientY;
var bounds = Sys.UI.Control.getBounds(_contextMenu.element);
if(!((x >= bounds.x) && (x <= bounds.x + bounds.width)
&& (y >= bounds.y) && (y <= bounds.y + bounds.height))) {
this.hideMenu();
}
}
// Public methods.
this.hideMenu = function() {
_popupBehavior.hide();
}
// getDescriptor.
this.getDescriptor = function() {
var td = AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'getDescriptor');
td.addProperty('contextMenuID', String);
td.addEvent('contextMenuShow', true);
td.addMethod('hideMenu');
return td;
}
}
AtlasNotes.UI.ContextMenuBehavior.registerClass('AtlasNotes.UI.ContextMenuBehavior', Sys.UI.Behavior);
Sys.TypeDescriptor.addType('script', 'contextMenuBehavior', AtlasNotes.UI.ContextMenuBehavior);
UPDATES:
- 03/21/2006 - Updated both the post and the code to the March CTP.
- 02/21/2006 - ContextMenuBehavior.js, fixed a problem with context menu positioning (thanks to Mirko).
- 01/24/2006 - ContextMenuBehavior.js, added the hideMenu() method in getDescriptor() to be used in declarative script.