@qooxdoo/framework
Version:
The JS Framework for Coders
582 lines (496 loc) • 15.5 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
************************************************************************ */
/**
* Each focus root delegates the focus handling to instances of the FocusHandler.
*/
qx.Class.define("qx.ui.core.FocusHandler", {
extend: qx.core.Object,
type: "singleton",
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
construct() {
super();
// Create data structure
this.__roots = {};
},
/*
***********************************************
PROPERTIES
***********************************************
*/
properties: {
/**
* Activate changing focus with the tab key (default: true)
*/
useTabNavigation: {
check: "Boolean",
init: true
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__roots: null,
__activeChild: null,
__focusedChild: null,
__currentRoot: null,
/**
* Connects to a top-level root element (which initially receives
* all events of the root). This are normally all page and application
* roots, but no inline roots (they are typically sitting inside
* another root).
*
* @param root {qx.ui.root.Abstract} Any root
*/
connectTo(root) {
// this.debug("Connect to: " + root);
root.addListener("keypress", this.__onKeyPress, this);
root.addListener("focusin", this._onFocusIn, this, true);
root.addListener("focusout", this._onFocusOut, this, true);
root.addListener("activate", this._onActivate, this, true);
root.addListener("deactivate", this._onDeactivate, this, true);
},
/**
* Registers a widget as a focus root. A focus root comes
* with an separate tab sequence handling.
*
* @param widget {qx.ui.core.Widget} The widget to register
*/
addRoot(widget) {
// this.debug("Add focusRoot: " + widget);
this.__roots[widget.toHashCode()] = widget;
},
/**
* Deregisters a previous added widget.
*
* @param widget {qx.ui.core.Widget} The widget to deregister
*/
removeRoot(widget) {
// this.debug("Remove focusRoot: " + widget);
delete this.__roots[widget.toHashCode()];
},
/**
* Get the active widget
*
* @return {qx.ui.core.Widget|null} The active widget or <code>null</code>
* if no widget is active
*/
getActiveWidget() {
return this.__activeChild;
},
/**
* Whether the given widget is the active one
*
* @param widget {qx.ui.core.Widget} The widget to check
* @return {Boolean} <code>true</code> if the given widget is active
*/
isActive(widget) {
return this.__activeChild == widget;
},
/**
* Get the focused widget
*
* @return {qx.ui.core.Widget|null} The focused widget or <code>null</code>
* if no widget has the focus
*/
getFocusedWidget() {
return this.__focusedChild;
},
/**
* Whether the given widget is the focused one.
*
* @param widget {qx.ui.core.Widget} The widget to check
* @return {Boolean} <code>true</code> if the given widget is focused
*/
isFocused(widget) {
return this.__focusedChild == widget;
},
/**
* Whether the given widgets acts as a focus root.
*
* @param widget {qx.ui.core.Widget} The widget to check
* @return {Boolean} <code>true</code> if the given widget is a focus root
*/
isFocusRoot(widget) {
return !!this.__roots[widget.toHashCode()];
},
/*
---------------------------------------------------------------------------
EVENT HANDLER
---------------------------------------------------------------------------
*/
/**
* Internal event handler for activate event.
*
* @param e {qx.event.type.Focus} Focus event
*/
_onActivate(e) {
var target = e.getTarget();
this.__activeChild = target;
//this.debug("active: " + target);
var root = this.__findFocusRoot(target);
if (root != this.__currentRoot) {
this.__currentRoot = root;
}
},
/**
* Internal event handler for deactivate event.
*
* @param e {qx.event.type.Focus} Focus event
*/
_onDeactivate(e) {
var target = e.getTarget();
if (this.__activeChild == target) {
this.__activeChild = null;
}
},
/**
* Internal event handler for focusin event.
*
* @param e {qx.event.type.Focus} Focus event
*/
_onFocusIn(e) {
var target = e.getTarget();
if (target != this.__focusedChild) {
this.__focusedChild = target;
target.visualizeFocus();
}
},
/**
* Internal event handler for focusout event.
*
* @param e {qx.event.type.Focus} Focus event
*/
_onFocusOut(e) {
var target = e.getTarget();
if (target == this.__focusedChild) {
this.__focusedChild = null;
target.visualizeBlur();
}
},
/**
* Internal event handler for TAB key.
*
* @param e {qx.event.type.KeySequence} Key event
*/
__onKeyPress(e) {
if (e.getKeyIdentifier() != "Tab" || !this.isUseTabNavigation()) {
return;
}
if (!this.__currentRoot) {
return;
}
// Stop all key-events with a TAB keycode
e.stopPropagation();
e.preventDefault();
// Support shift key to reverse widget detection order
var current = this.__focusedChild;
if (!e.isShiftPressed()) {
var next = current
? this.__getWidgetAfter(current)
: this.__getFirstWidget();
} else {
var next = current
? this.__getWidgetBefore(current)
: this.__getLastWidget();
}
// If there was a widget found, focus it
if (next) {
next.tabFocus();
}
},
/*
---------------------------------------------------------------------------
UTILS
---------------------------------------------------------------------------
*/
/**
* Finds the next focus root, starting with the given widget.
*
* @param widget {qx.ui.core.Widget} The widget to find a focus root for.
* @return {qx.ui.core.Widget|null} The focus root for the given widget or
* <code>true</code> if no focus root could be found
*/
__findFocusRoot(widget) {
var roots = this.__roots;
while (widget) {
if (roots[widget.toHashCode()]) {
return widget;
}
widget = widget.getLayoutParent();
}
return null;
},
/*
---------------------------------------------------------------------------
TAB SUPPORT IMPLEMENTATION
---------------------------------------------------------------------------
*/
/**
* Compares the order of two widgets
*
* @param widget1 {qx.ui.core.Widget} Widget A
* @param widget2 {qx.ui.core.Widget} Widget B
* @return {Integer} A sort() compatible integer with values
* small than 0, exactly 0 or bigger than 0.
*/
__compareTabOrder(widget1, widget2) {
if (widget1 === widget2) {
return 0;
}
// Sort-Check #1: Tab-Index
var tab1 = widget1.getTabIndex() || 0;
var tab2 = widget2.getTabIndex() || 0;
if (tab1 != tab2) {
return tab1 - tab2;
}
// Computing location
var el1 = widget1.getContentElement().getDomElement();
var el2 = widget2.getContentElement().getDomElement();
var Location = qx.bom.element.Location;
var loc1 = Location.get(el1);
var loc2 = Location.get(el2);
// Sort-Check #2: Top-Position
if (loc1.top != loc2.top) {
return loc1.top - loc2.top;
}
// Sort-Check #3: Left-Position
if (loc1.left != loc2.left) {
return loc1.left - loc2.left;
}
// Sort-Check #4: zIndex
var z1 = widget1.getZIndex();
var z2 = widget2.getZIndex();
if (z1 != z2) {
return z1 - z2;
}
return 0;
},
/**
* Returns the first widget.
*
* @return {qx.ui.core.Widget} Returns the first (positioned) widget from
* the current root.
*/
__getFirstWidget() {
return this.__getFirst(this.__currentRoot, null);
},
/**
* Returns the last widget.
*
* @return {qx.ui.core.Widget} Returns the last (positioned) widget from
* the current root.
*/
__getLastWidget() {
return this.__getLast(this.__currentRoot, null);
},
/**
* Returns the widget after the given one.
*
* @param widget {qx.ui.core.Widget} Widget to start with
* @return {qx.ui.core.Widget} The found widget.
*/
__getWidgetAfter(widget) {
var root = this.__currentRoot;
if (root == widget) {
return this.__getFirstWidget();
}
while (widget && widget.getAnonymous()) {
widget = widget.getLayoutParent();
}
if (widget == null) {
return [];
}
var result = [];
this.__collectAllAfter(root, widget, result);
result.sort(this.__compareTabOrder);
var len = result.length;
return len > 0 ? result[0] : this.__getFirstWidget();
},
/**
* Returns the widget before the given one.
*
* @param widget {qx.ui.core.Widget} Widget to start with
* @return {qx.ui.core.Widget} The found widget.
*/
__getWidgetBefore(widget) {
var root = this.__currentRoot;
if (root == widget) {
return this.__getLastWidget();
}
while (widget && widget.getAnonymous()) {
widget = widget.getLayoutParent();
}
if (widget == null) {
return [];
}
var result = [];
this.__collectAllBefore(root, widget, result);
result.sort(this.__compareTabOrder);
var len = result.length;
return len > 0 ? result[len - 1] : this.__getLastWidget();
},
/*
---------------------------------------------------------------------------
INTERNAL API USED BY METHODS ABOVE
---------------------------------------------------------------------------
*/
/**
* Collects all widgets which are after the given widget in
* the given parent widget. Append all found children to the
* <code>list</code>.
*
* @param parent {qx.ui.core.Widget} Parent widget
* @param widget {qx.ui.core.Widget} Child widget to start with
* @param result {Array} Result list
*/
__collectAllAfter(parent, widget, result) {
var children = parent.getLayoutChildren();
var child;
for (var i = 0, l = children.length; i < l; i++) {
child = children[i];
// Filter spacers etc.
if (!(child instanceof qx.ui.core.Widget)) {
continue;
}
if (
!this.isFocusRoot(child) &&
child.isEnabled() &&
child.isVisible()
) {
if (child.isTabable() && this.__compareTabOrder(widget, child) < 0) {
result.push(child);
}
this.__collectAllAfter(child, widget, result);
}
}
},
/**
* Collects all widgets which are before the given widget in
* the given parent widget. Append all found children to the
* <code>list</code>.
*
* @param parent {qx.ui.core.Widget} Parent widget
* @param widget {qx.ui.core.Widget} Child widget to start with
* @param result {Array} Result list
*/
__collectAllBefore(parent, widget, result) {
var children = parent.getLayoutChildren();
var child;
for (var i = 0, l = children.length; i < l; i++) {
child = children[i];
// Filter spacers etc.
if (!(child instanceof qx.ui.core.Widget)) {
continue;
}
if (
!this.isFocusRoot(child) &&
child.isEnabled() &&
child.isVisible()
) {
if (child.isTabable() && this.__compareTabOrder(widget, child) > 0) {
result.push(child);
}
this.__collectAllBefore(child, widget, result);
}
}
},
/**
* Find first (positioned) widget. (Sorted by coordinates, zIndex, etc.)
*
* @param parent {qx.ui.core.Widget} Parent widget
* @param firstWidget {qx.ui.core.Widget?null} Current first widget
* @return {qx.ui.core.Widget} The first (positioned) widget
*/
__getFirst(parent, firstWidget) {
var children = parent.getLayoutChildren();
var child;
for (var i = 0, l = children.length; i < l; i++) {
child = children[i];
// Filter spacers etc.
if (!(child instanceof qx.ui.core.Widget)) {
continue;
}
// Ignore focus roots completely
if (
!this.isFocusRoot(child) &&
child.isEnabled() &&
child.isVisible()
) {
if (child.isTabable()) {
if (
firstWidget == null ||
this.__compareTabOrder(child, firstWidget) < 0
) {
firstWidget = child;
}
}
// Deep iteration into children hierarchy
firstWidget = this.__getFirst(child, firstWidget);
}
}
return firstWidget;
},
/**
* Find last (positioned) widget. (Sorted by coordinates, zIndex, etc.)
*
* @param parent {qx.ui.core.Widget} Parent widget
* @param lastWidget {qx.ui.core.Widget?null} Current last widget
* @return {qx.ui.core.Widget} The last (positioned) widget
*/
__getLast(parent, lastWidget) {
var children = parent.getLayoutChildren();
var child;
for (var i = 0, l = children.length; i < l; i++) {
child = children[i];
// Filter spacers etc.
if (!(child instanceof qx.ui.core.Widget)) {
continue;
}
// Ignore focus roots completely
if (
!this.isFocusRoot(child) &&
child.isEnabled() &&
child.isVisible()
) {
if (child.isTabable()) {
if (
lastWidget == null ||
this.__compareTabOrder(child, lastWidget) > 0
) {
lastWidget = child;
}
}
// Deep iteration into children hierarchy
lastWidget = this.__getLast(child, lastWidget);
}
}
return lastWidget;
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct() {
this._disposeMap("__roots");
this.__focusedChild = this.__activeChild = this.__currentRoot = null;
}
});