I’m building a new project at work that’s going to require me to be more diligent with tabs than normal. I need to have specific groups of controls that you can tab around, but not tab between. I started thinking about a small, simple solution, but then I remembered all the trouble I had on this last project. It had pop-up forms (in Flash, not JavaScript), but when you’d tab to the end of the form, the focus would start going to items in the background that were supposed to be disabled. It was a really annoying bug.
So to fix my problem, I started looking up options for focus management in AS3. I saw a few things floating around online, but most were either Flex based, cumbersome, or not as easy to implement as I wanted. So, being a lover of utility classes, I decided to build my own.
I wanted a very specific set of features, and listed them out:
- Define groups of tab-able items
- Groups can be defined by any object, not just a string name
- Define the order of items within a group
- Allow items to be in more than one group
- Maintain one active group at a time
- Quickly toggle between groups
- Clean up nicely
Below is the class I came up with for my first draft. It uses a couple other utility classes I’ve built in the past (included in the ZIP below).
package org.tomasino.accessibility
{
import flash.display.InteractiveObject;
import flash.utils.Dictionary;
public class TabManager
{
private static var _inst:TabManager;
private var _hash:Dictionary = new Dictionary(true);
private var _activeCategory:Object;
public function TabManager (_singletonEnforcer:SingletonEnforcer):void { }
public static function get inst ():TabManager
{
if (!_inst)
{
_inst = new TabManager (new SingletonEnforcer ());
}
return _inst;
}
public function addTabItem (category:Object, io:InteractiveObject, index:int = -1):void
{
// Set this as active category if this is the first item added
if (!_activeCategory) _activeCategory = category;
var cat:Array;
// If no existing category, create it
if (exists(category))
{
cat = _hash[category] as Array;
}
else
{
cat = new Array ();
_hash[category] = cat;
}
var existingIndex:int = cat.indexOf (io);
if (existingIndex == -1)
{
// If item is already in category...
if ((index >= 0) && (index < cat.length))
{
// Put it in a valid index
cat.splice (index, 0, io);
}
else
{
// Put it at the end
cat.push (io);
}
}
else
{
if (existingIndex != index)
{
cat.splice (existingIndex, 1);
if ((index >= 0) && (index < cat.length))
{
// Put it in a valid index
cat.splice (index, 0, io);
}
else
{
// Put it at the end
cat.push (io);
}
}
}
if (_activeCategory != category)
{
var activeCategory:Array = _hash[_activeCategory] as Array;
if (activeCategory.indexOf (io) == -1)
io.tabEnabled = false;
}
else
{
io.tabEnabled = true;
}
//io.focusRect = false;
updateTabOrder (category);
}
public function removeTabItem (io:InteractiveObject):void
{
for (var key:Object in _hash )
{
var cat:Array = _hash[key] as Array;
if (cat)
{
var existingIndex:int = cat.indexOf (io);
if (existingIndex != -1)
cat.splice (existingIndex, 1);
}
}
}
public function removeTabItemFromCategory (category:Object, io:InteractiveObject):void
{
var cat:Array = _hash[category] as Array;
if (cat)
{
var existingIndex:int = cat.indexOf (io);
if (existingIndex != -1)
cat.splice (existingIndex, 1);
}
}
public function removeCategory (category:Object):void
{
_hash[category] = null;
}
public function cleanup ():void
{
for (var k:Object in _hash )
{
var key:Array = k as Array;
if (key)
{
for (var i:int = key.length - 1; i >= 0; i--)
{
if (key[i] == null)
{
key.splice (i, 1);
}
}
}
}
}
public function get activeCategory ():Object { return _activeCategory; }
public function set activeCategory (category:Object):void
{
if (exists(category))
{
if (category != _activeCategory)
{
deactivate (_activeCategory);
activate (category);
}
}
else
{
trace ('Category does not exist:', category);
}
}
private function deactivate (category:Object):void
{
if (_activeCategory == category) _activeCategory = null;
if (category)
{
var cat:Array = _hash[category] as Array;
if (cat)
{
for (var i:int = 0; i < cat.length; ++i)
{
var io:InteractiveObject = cat[i];
if (io)
{
io.tabEnabled = false;
}
}
}
}
}
private function activate (category:Object):void
{
_activeCategory = category;
if (category)
{
var cat:Array = _hash[category] as Array;
if (cat)
{
for (var i:int = 0; i < cat.length; ++i)
{
var io:InteractiveObject = cat[i];
try
{
//io.stage.focus = io;
}
catch (e:Error)
{
// Fail quietly
}
if (io)
{
io.tabEnabled = true;
}
}
}
}
}
private function updateTabOrder (category:Object):void
{
if (category)
{
var cat:Array = _hash[category] as Array;
if (cat)
{
for (var i:int = 0; i < cat.length; ++i)
{
var io:InteractiveObject = cat[i];
io.tabIndex = i;
}
}
}
}
private function exists (category:Object):Boolean
{
return (_hash[category] == null) ? false : true;
}
}
}
internal class SingletonEnforcer
{
public function SingletonEnforcer ()
{
// there can be only one
}
}
All-in-all, though, I think it’s a pretty solid start at a good tab management system.
The biggest issues for implementation are:
- Focus Rectangles are still default yellow and there’s no good way to style them otherwise automatically
- You must be diligent about adding every tab-able item to the manager. If you miss something, the TabManager won’t know about it and it will remain in the normal tab order.
Those two bits are less than ideal, but certainly manageable.
One day I would like to tackle the optimization and add some new features. I think it would be neat to have some auto-focus detection or something that could toggle the tab system for you, if you wanted, perhaps driven by a boolean. That way, if you were using one tab group and you manually clicked with your mouse to another, it would toggle to use the new group’s tab order.
Anyone out there have any other ideas or requests that could make this thing more usable? Either way, the files are down below. Feel free to grab and play. Please comment if you find it useful or interesting.
There’s no license on this or any of my utility code (anything in the com.tomasino packages). Feel free to use it or modify it at your own discretion. If you find something useful I’d love to know about it. Thanks.
Example Code, source, and FLA: here
UPDATE: I’ve done some surgery to the class to remove the excess dependencies and fix a few bugs. The example project will be updated shortly as well.
Update: Please make sure to get the latest version of this code from my github repository.
Hey, this looks like an awesome utility. Focus management has always caused me a lot of pitfalls. The yellow box has plagued me for ages. Seriously Adobe, what would be so wrong with giving us the ability to re-style the focus rectangle???
Would love for you to post a swf / fla showing a basic implementation!
Great article.
@comatose – Thanks for the comment. The ZIP file at the bottom of the post contains a sample implementation in the FLA. I saved the file back to Flash CS3 so more people can view it. I hope you find it useful!
[...] For those who care, I used a slightly different singleton implementation on this class than on my TabManager, not for any particular reason, but just because I like to mix it up. The roll of the Log class is [...]
thank you for presenting your code.
This is exactly the solution that came to my mind after reading about all the (im)possibilities in this subject of managing focus for more complex applications… BUT thinking of a solution is still very far away from actually having built one. You are a hero. Hope to be back here with a pureMVC version.
@Bert – Seeing your comment shamed me into getting off my lazy butt and updating the class. I’ve removed some unnecessary dependencies and included a couple fixes for bugs that I ran across when using it on some projects. The sample has been updated, as has the class itself. If you can make a good implementation for PureMVC, I’d be excited to see it! Good luck.
[...] TAGS: None My programming posts have been moved to my new coding blog – Tomasino Labs. This specific post can be found here. [...]