Bug 472662 - no reorder event for most display property & DOM changes, r=marcoz, davidb

This commit is contained in:
Alexander Surkov 2009-02-05 14:23:18 +08:00
parent e1817688f3
commit 9571a8215e
7 changed files with 381 additions and 63 deletions

View File

@ -46,6 +46,7 @@
#include "nsAccessibleEventData.h"
#include "nsHyperTextAccessible.h"
#include "nsAccessibilityAtoms.h"
#include "nsAccessibleTreeWalker.h"
#include "nsAccessible.h"
#include "nsARIAMap.h"
@ -356,6 +357,33 @@ nsAccUtils::FireAccEvent(PRUint32 aEventType, nsIAccessible *aAccessible,
return pAccessible->FireAccessibleEvent(event);
}
PRBool
nsAccUtils::HasAccessibleChildren(nsIDOMNode *aNode)
{
if (!aNode)
return PR_FALSE;
nsCOMPtr<nsIContent> content(do_QueryInterface(aNode));
if (!content)
return PR_FALSE;
nsCOMPtr<nsIPresShell> presShell = nsCoreUtils::GetPresShellFor(aNode);
if (!presShell)
return PR_FALSE;
nsIFrame *frame = presShell->GetPrimaryFrameFor(content);
if (!frame)
return PR_FALSE;
nsCOMPtr<nsIWeakReference> weakShell(do_GetWeakReference(presShell));
nsAccessibleTreeWalker walker(weakShell, aNode, PR_FALSE);
walker.mState.frame = frame;
walker.GetFirstChild();
return walker.mState.accessible ? PR_TRUE : PR_FALSE;
}
already_AddRefed<nsIAccessible>
nsAccUtils::GetAncestorWithRole(nsIAccessible *aDescendant, PRUint32 aRole)
{

View File

@ -151,6 +151,11 @@ public:
static nsresult FireAccEvent(PRUint32 aEventType, nsIAccessible *aAccessible,
PRBool aIsAsynch = PR_FALSE);
/**
* Return true if the given DOM node contains accessible children.
*/
static PRBool HasAccessibleChildren(nsIDOMNode *aNode);
/**
* If an ancestor in this document exists with the given role, return it
* @param aDescendant Descendant to start search with

View File

@ -318,6 +318,11 @@ nsAccEvent::ApplyEventRules(nsCOMArray<nsIAccessibleEvent> &aEventsToFire)
continue; // Do not need to check
if (thisEvent->mDOMNode == tailEvent->mDOMNode) {
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameSource(thisEvent, tailEvent);
continue;
}
// Dupe
thisEvent->mEventRule = nsAccEvent::eDoNotEmit;
continue;
@ -325,6 +330,11 @@ nsAccEvent::ApplyEventRules(nsCOMArray<nsIAccessibleEvent> &aEventsToFire)
if (nsCoreUtils::IsAncestorOf(tailEvent->mDOMNode,
thisEvent->mDOMNode)) {
// thisDOMNode is a descendant of tailDOMNode
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameTree(tailEvent, thisEvent);
continue;
}
// Do not emit thisEvent, also apply this result to sibling
// nodes of thisDOMNode.
thisEvent->mEventRule = nsAccEvent::eDoNotEmit;
@ -335,6 +345,11 @@ nsAccEvent::ApplyEventRules(nsCOMArray<nsIAccessibleEvent> &aEventsToFire)
if (nsCoreUtils::IsAncestorOf(thisEvent->mDOMNode,
tailEvent->mDOMNode)) {
// tailDOMNode is a descendant of thisDOMNode
if (thisEvent->mEventType == nsIAccessibleEvent::EVENT_REORDER) {
CoalesceReorderEventsFromSameTree(thisEvent, tailEvent);
continue;
}
// Do not emit tailEvent, also apply this result to sibling
// nodes of tailDOMNode.
tailEvent->mEventRule = nsAccEvent::eDoNotEmit;
@ -387,7 +402,92 @@ nsAccEvent::ApplyToSiblings(nsCOMArray<nsIAccessibleEvent> &aEventsToFire,
}
}
/* static */
void
nsAccEvent::CoalesceReorderEventsFromSameSource(nsAccEvent *aAccEvent1,
nsAccEvent *aAccEvent2)
{
// Do not emit event2 if event1 is unconditional.
nsAccReorderEvent* reorderEvent1 = nsnull;
CallQueryInterface(aAccEvent1, &reorderEvent1);
if (reorderEvent1->IsUnconditionalEvent()) {
aAccEvent2->mEventRule = nsAccEvent::eDoNotEmit;
return;
}
// Do not emit event1 if event2 is unconditional.
nsAccReorderEvent* reorderEvent2 = nsnull;
CallQueryInterface(aAccEvent2, &reorderEvent2);
if (reorderEvent2->IsUnconditionalEvent()) {
aAccEvent1->mEventRule = nsAccEvent::eDoNotEmit;
return;
}
// Do not emit event2 if event1 is valid, otherwise do not emit event1.
if (reorderEvent1->HasAccessibleInReasonSubtree())
aAccEvent2->mEventRule = nsAccEvent::eDoNotEmit;
else
aAccEvent1->mEventRule = nsAccEvent::eDoNotEmit;
}
void
nsAccEvent::CoalesceReorderEventsFromSameTree(nsAccEvent *aAccEvent,
nsAccEvent *aDescendantAccEvent)
{
// Do not emit descendant event if this event is unconditional.
nsAccReorderEvent* reorderEvent = nsnull;
CallQueryInterface(aAccEvent, &reorderEvent);
if (reorderEvent->IsUnconditionalEvent()) {
aDescendantAccEvent->mEventRule = nsAccEvent::eDoNotEmit;
return;
}
// Do not emit descendant event if this event is valid otherwise do not emit
// this event.
if (reorderEvent->HasAccessibleInReasonSubtree())
aDescendantAccEvent->mEventRule = nsAccEvent::eDoNotEmit;
else
aAccEvent->mEventRule = nsAccEvent::eDoNotEmit;
}
////////////////////////////////////////////////////////////////////////////////
// nsAccReorderEvent
NS_IMPL_ISUPPORTS_INHERITED1(nsAccReorderEvent, nsAccEvent,
nsAccReorderEvent)
nsAccReorderEvent::nsAccReorderEvent(nsIAccessible *aAccTarget,
PRBool aIsAsynch,
PRBool aIsUnconditional,
nsIDOMNode *aReasonNode) :
nsAccEvent(::nsIAccessibleEvent::EVENT_REORDER, aAccTarget,
aIsAsynch, nsAccEvent::eCoalesceFromSameSubtree),
mUnconditionalEvent(aIsUnconditional), mReasonNode(aReasonNode)
{
}
PRBool
nsAccReorderEvent::IsUnconditionalEvent()
{
return mUnconditionalEvent;
}
PRBool
nsAccReorderEvent::HasAccessibleInReasonSubtree()
{
if (!mReasonNode)
return PR_FALSE;
nsCOMPtr<nsIAccessible> accessible;
nsAccessNode::GetAccService()->GetAccessibleFor(mReasonNode,
getter_AddRefs(accessible));
return accessible || nsAccUtils::HasAccessibleChildren(mReasonNode);
}
////////////////////////////////////////////////////////////////////////////////
// nsAccStateChangeEvent
NS_IMPL_ISUPPORTS_INHERITED1(nsAccStateChangeEvent, nsAccEvent,
nsIAccessibleStateChangeEvent)

View File

@ -187,10 +187,61 @@ private:
PRUint32 aStart, PRUint32 aEnd,
PRUint32 aEventType, nsIDOMNode* aDOMNode,
EEventRule aEventRule);
/**
* Do not emit one of two given reorder events fired for the same DOM node.
*/
static void CoalesceReorderEventsFromSameSource(nsAccEvent *aAccEvent1,
nsAccEvent *aAccEvent2);
/**
* Do not emit one of two given reorder events fired for DOM nodes in the case
* when one DOM node is in parent chain of second one.
*/
static void CoalesceReorderEventsFromSameTree(nsAccEvent *aAccEvent,
nsAccEvent *aDescendantAccEvent);
};
NS_DEFINE_STATIC_IID_ACCESSOR(nsAccEvent, NS_ACCEVENT_IMPL_CID)
#define NS_ACCREORDEREVENT_IMPL_CID \
{ /* f2629eb8-2458-4358-868c-3912b15b767a */ \
0xf2629eb8, \
0x2458, \
0x4358, \
{ 0x86, 0x8c, 0x39, 0x12, 0xb1, 0x5b, 0x76, 0x7a } \
}
class nsAccReorderEvent : public nsAccEvent
{
public:
nsAccReorderEvent(nsIAccessible *aAccTarget, PRBool aIsAsynch,
PRBool aIsUnconditional, nsIDOMNode *aReasonNode);
NS_DECLARE_STATIC_IID_ACCESSOR(NS_ACCREORDEREVENT_IMPL_CID)
NS_DECL_ISUPPORTS_INHERITED
/**
* Return true if event is unconditional, i.e. must be fired.
*/
PRBool IsUnconditionalEvent();
/**
* Return true if changed DOM node has accessible in its tree.
*/
PRBool HasAccessibleInReasonSubtree();
private:
PRBool mUnconditionalEvent;
nsCOMPtr<nsIDOMNode> mReasonNode;
};
NS_DEFINE_STATIC_IID_ACCESSOR(nsAccReorderEvent, NS_ACCREORDEREVENT_IMPL_CID)
class nsAccStateChangeEvent: public nsAccEvent,
public nsIAccessibleStateChangeEvent
{

View File

@ -1704,6 +1704,18 @@ NS_IMETHODIMP nsDocAccessible::FlushPendingEvents()
}
}
}
else if (eventType == nsIAccessibleEvent::EVENT_REORDER) {
// Fire reorder event if it's unconditional (see InvalidateCacheSubtree
// method) or if changed node (that is the reason of this reorder event)
// is accessible or has accessible children.
nsAccReorderEvent* reorderEvent = nsnull;
CallQueryInterface(accessibleEvent, &reorderEvent);
if (reorderEvent->IsUnconditionalEvent() ||
reorderEvent->HasAccessibleInReasonSubtree()) {
nsAccEvent::PrepareForEvent(accessibleEvent);
FireAccessibleEvent(accessibleEvent);
}
}
else {
// The input state was previously stored with the nsIAccessibleEvent,
// so use that state now when firing the event
@ -2050,15 +2062,29 @@ NS_IMETHODIMP nsDocAccessible::InvalidateCacheSubtree(nsIContent *aChild,
FireValueChangeForTextFields(containerAccessible);
if (childAccessible) {
// Fire an event so the MSAA clients know the children have changed. Also
// the event is used internally by MSAA part.
nsCOMPtr<nsIAccessibleEvent> reorderEvent =
new nsAccEvent(nsIAccessibleEvent::EVENT_REORDER, containerAccessible,
isAsynch, nsAccEvent::eCoalesceFromSameSubtree);
NS_ENSURE_TRUE(reorderEvent, NS_ERROR_OUT_OF_MEMORY);
FireDelayedAccessibleEvent(reorderEvent);
}
// Fire an event so the MSAA clients know the children have changed. Also
// the event is used internally by MSAA part.
// We need to fire reorder event for accessible parent of the changed node if
// the changed node is accessible or has accessible children. In this case
// we fire delayed unconditional reorder event which means it will be fired
// after timeout in any case (of course if it won't be coalesced from event
// queue). But at this point in the case of show events accessible object may
// be not created for generally accessible changed node (because its frame may
// be not constructed yet). Therefore we can to check whether the changed node
// is accessible or has accessible children after timeout only. In the case we
// fire conditional reorder event.
PRBool isUnconditionalEvent = childAccessible ||
aChild && nsAccUtils::HasAccessibleChildren(childNode);
nsCOMPtr<nsIAccessibleEvent> reorderEvent =
new nsAccReorderEvent(containerAccessible, isAsynch,
isUnconditionalEvent,
aChild ? childNode : nsnull);
NS_ENSURE_TRUE(reorderEvent, NS_ERROR_OUT_OF_MEMORY);
FireDelayedAccessibleEvent(reorderEvent);
return NS_OK;
}

View File

@ -1,6 +1,11 @@
////////////////////////////////////////////////////////////////////////////////
// General
/**
* Set up this variable to dump events into DOM.
*/
var gA11yEventDumpID = "";
/**
* Register accessibility event listener.
*
@ -203,6 +208,20 @@ function eventQueue(aEventType)
// We wait for events in order specified by eventSeq variable.
var idx = this.mEventSeqIdx + 1;
if (gA11yEventDumpID) { // debug stuff
var eventType = this.mEventSeq[idx][0];
var target = this.mEventSeq[idx][1];
var info = "Event queue processing. Event type: ";
info += gAccRetrieval.getStringEventType(eventType) + ". Target: ";
info += (target.localName ? target.localName : target);
if (target.nodeType == nsIDOMNode.ELEMENT_NODE &&
target.hasAttribute("id"))
info += " '" + target.getAttribute("id") + "'";
dumpInfoToDOM(info);
}
if (aEvent.eventType == this.mEventSeq[idx][0] &&
aEvent.DOMNode == this.mEventSeq[idx][1]) {
@ -242,7 +261,7 @@ function eventQueue(aEventType)
if (this.mEventSeq) {
aInvoker.wasCaught = new Array(this.mEventSeq.length);
for (var idx = 0; idx < this.mEventSeq.length; idx++)
addA11yEventListener(this.mEventSeq[idx][0], this);
}
@ -279,7 +298,6 @@ var gObserverService = null;
var gA11yEventListeners = {};
var gA11yEventApplicantsCount = 0;
var gA11yEventDumpID = ""; // set up this variable to dump events into DOM.
var gA11yEventObserver =
{
@ -292,9 +310,8 @@ var gA11yEventObserver =
var listenersArray = gA11yEventListeners[event.eventType];
if (gA11yEventDumpID) { // debug stuff
var dumpElm = document.getElementById(gA11yEventDumpID);
var target = event.DOMNode;
var dumpElm = document.getElementById(gA11yEventDumpID);
var parent = target;
while (parent && parent != dumpElm)
@ -302,14 +319,17 @@ var gA11yEventObserver =
if (parent != dumpElm) {
var type = gAccRetrieval.getStringEventType(event.eventType);
var info = "event type: " + type + ", target: " + target.localName;
var info = "Event type: " + type + ". Target: ";
info += (target.localName ? target.localName : target);
if (target.nodeType == nsIDOMNode.ELEMENT_NODE &&
target.hasAttribute("id"))
info += " '" + target.getAttribute("id") + "'";
if (listenersArray)
info += ", registered listeners count is " + listenersArray.length;
info += ". Listeners count: " + listenersArray.length;
var div = document.createElement("div");
div.textContent = info;
dumpElm.appendChild(div);
dumpInfoToDOM(info);
}
}
@ -363,3 +383,11 @@ function removeA11yEventListener(aEventType, aEventHandler)
return true;
}
function dumpInfoToDOM(aInfo)
{
var dumpElm = document.getElementById(gA11yEventDumpID);
var div = document.createElement("div");
div.textContent = aInfo;
dumpElm.appendChild(div);
}

View File

@ -36,38 +36,71 @@
var kHideEvents = kHideEvent | kReorderEvent;
var kHideAndShowEvents = kHideEvents | kShowEvent;
function mutateA11yTree(aNodeOrID, aEventTypes, aDoNotExpectEvents,
aIsDOMChange)
/**
* Base class to test mutation a11y events.
*
* @param aNodeOrID [in] node invoker's action is executed for
* @param aEventTypes [in] events to register (see constants above)
* @param aDoNotExpectEvents [in] boolean indicates if events are expected
* @param aIsDOMChange [in] boolean indicates if these are DOM events
* layout events.
*/
function mutateA11yTree(aNodeOrID, aEventTypes,
aDoNotExpectEvents, aIsDOMChange)
{
// Interface
this.DOMNode = getNode(aNodeOrID);
this.doNotExpectEvents = aDoNotExpectEvents;
this.eventSeq = [];
if (aIsDOMChange) {
if (aEventTypes & kHideEvent)
this.eventSeq.push([nsIAccessibleEvent.EVENT_DOM_DESTROY,
this.DOMNode]);
if (aEventTypes & kShowEvent)
this.eventSeq.push([nsIAccessibleEvent.EVENT_DOM_CREATE,
this.DOMNode]);
} else {
if (aEventTypes & kHideEvent)
this.eventSeq.push([nsIAccessibleEvent.EVENT_ASYNCH_HIDE,
this.DOMNode]);
if (aEventTypes & kShowEvent)
this.eventSeq.push([nsIAccessibleEvent.EVENT_ASYNCH_SHOW,
this.DOMNode]);
this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget)
{
var type = this.getA11yEventType(aEventType);
for (var idx = 0; idx < this.eventSeq.length; idx++) {
if (this.eventSeq[idx][0] == type) {
this.eventSeq[idx][1] = aTarget;
break;
}
}
}
if (aEventTypes & kReorderEvent)
this.eventSeq.push([nsIAccessibleEvent.EVENT_REORDER,
this.DOMNode.parentNode]);
// Implementation
this.getA11yEventType = function mutateA11yTree_getA11yEventType(aEventType)
{
if (aEventType == kReorderEvent)
return nsIAccessibleEvent.EVENT_REORDER;
this.doNotExpectEvents = aDoNotExpectEvents;
if (this.mIsDOMChange) {
if (aEventType == kHideEvent)
return nsIAccessibleEvent.EVENT_DOM_DESTROY;
if (aEventType == kShowEvent)
return nsIAccessibleEvent.EVENT_DOM_CREATE;
} else {
if (aEventType == kHideEvent)
return nsIAccessibleEvent.EVENT_ASYNCH_HIDE;
if (aEventType == kShowEvent)
return nsIAccessibleEvent.EVENT_ASYNCH_SHOW;
}
}
this.mIsDOMChange = aIsDOMChange;
if (aEventTypes & kHideEvent)
this.eventSeq.push([this.getA11yEventType(kHideEvent), this.DOMNode]);
if (aEventTypes & kShowEvent)
this.eventSeq.push([this.getA11yEventType(kShowEvent), this.DOMNode]);
if (aEventTypes & kReorderEvent)
this.eventSeq.push([this.getA11yEventType(kReorderEvent),
this.DOMNode.parentNode]);
}
/**
* Change CSS style for the given node.
*/
function changeStyle(aNodeOrID, aProp, aValue, aEventTypes)
{
this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false, false);
@ -83,6 +116,9 @@
}
}
/**
* Change class name for the given node.
*/
function changeClass(aParentNodeOrID, aNodeOrID, aClassName, aEventTypes)
{
this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false, false);
@ -100,7 +136,11 @@
this.parentDOMNode = getNode(aParentNodeOrID);
}
function cloneAndAppendToDOM(aNodeOrID, aEventTypes)
/**
* Clone the node and append it to its parent.
*/
function cloneAndAppendToDOM(aNodeOrID, aEventTypes,
aTargetFunc, aReorderTargetFunc)
{
var eventTypes = aEventTypes || kShowEvents;
var doNotExpectEvents = (aEventTypes == kNoEvents);
@ -112,7 +152,16 @@
{
var newElm = this.DOMNode.cloneNode(true);
newElm.removeAttribute('id');
this.eventSeq[0][1] = newElm;
var target = this.mTargetFunc ?
this.mTargetFunc.call(null, newElm) : newElm;
this.setTarget(kShowEvent, target);
if (this.mReorderTargetFunc) {
var reorderTarget = this.mReorderTargetFunc.call(null, this.DOMNode);
this.setTarget(kReorderEvent, reorderTarget);
}
this.DOMNode.parentNode.appendChild(newElm);
}
@ -120,9 +169,16 @@
{
return aNodeOrID + " clone and append to DOM.";
}
this.mTargetFunc = aTargetFunc;
this.mReorderTargetFunc = aReorderTargetFunc;
}
function removeFromDOM(aNodeOrID, aEventTypes)
/**
* Removes the node from DOM.
*/
function removeFromDOM(aNodeOrID, aEventTypes,
aTargetFunc, aReorderTargetFunc)
{
var eventTypes = aEventTypes || kHideEvents;
var doNotExpectEvents = (aEventTypes == kNoEvents);
@ -139,8 +195,18 @@
{
return aNodeOrID + " remove from DOM.";
}
if (aTargetFunc && (eventTypes & kHideEvent))
this.setTarget(kHideEvent, aTargetFunc.call(null, this.DOMNode));
if (aReorderTargetFunc && (eventTypes & kReorderEvent))
this.setTarget(kReorderEvent,
aReorderTargetFunc.call(null, this.DOMNode));
}
/**
* Clone the node and replace the original node by cloned one.
*/
function cloneAndReplaceInDOM(aNodeOrID)
{
this.__proto__ = new mutateA11yTree(aNodeOrID, kHideAndShowEvents,
@ -160,6 +226,9 @@
}
}
function getFirstChild(aNode) { return aNode.firstChild; }
function getParent(aNode) { return aNode.parentNode; }
/**
* Do tests.
*/
@ -175,9 +244,7 @@
var id = "link1";
getAccessible(id); // ensure accessible is created
gQueue.push(new changeStyle(id, "display", "none", kHideEvents));
// XXX: bug 472662 - there is no expected reorder event
gQueue.push(new changeStyle(id, "display", "inline", kShowEvent));
gQueue.push(new changeStyle(id, "display", "inline", kShowEvents));
// Show/hide events by changing of visibility style of accessible DOM node
// from 'visible' to 'hidden', 'hidden' to 'visible'.
@ -201,8 +268,7 @@
// Show/hide events by adding new accessible DOM node and removing old one.
var id = "link5";
// XXX: bug 472662 - there is no expected reorder event
gQueue.push(new cloneAndAppendToDOM(id, kShowEvent));
gQueue.push(new cloneAndAppendToDOM(id));
gQueue.push(new removeFromDOM(id));
// No show/hide events by adding new not accessible DOM node and removing
@ -214,17 +280,25 @@
// Show/hide events by adding new accessible DOM node and removing
// old one, there is reorder event for their parent.
var id = "child2";
gQueue.push(new cloneAndAppendToDOM(id, kShowEvent));
gQueue.push(new cloneAndAppendToDOM(id));
gQueue.push(new removeFromDOM(id));
// Show/hide events by adding new DOM node containing accessible DOM and
// removing old one, there is reorder event for their parent.
var id = "child3";
gQueue.push(new cloneAndAppendToDOM(id, kShowEvents, getFirstChild,
getParent));
// XXX: bug 475503, there is no hide event
gQueue.push(new removeFromDOM(id, kReorderEvent, getFirstChild, getParent));
// Show/hide events by creating new accessible DOM node and replacing
// old one.
// XXX: bug 472810
// gQueue.push(new cloneAndReplaceInDOM("link6"));
// Show/hide events by changing class name on the parent node.
// XXX: bug 472662 - there is no expected reorder event
gQueue.push(new changeClass("container2", "link7", "", kShowEvent));
gQueue.push(new changeClass("container2", "link7", "", kShowEvents));
gQueue.push(new changeClass("container2", "link7", "displayNone",
kHideEvents));
gQueue.push(new changeClass("container3", "link8", "", kShowEvents));
@ -256,19 +330,25 @@
<div id="content" style="display: none"></div>
<pre id="test">
</pre>
<div id="eventdump"/>
<div id="eventdump"></div>
<a id="link1" href="http://www.google.com">Link #1</a>
<a id="link2" href="http://www.google.com">Link #2</a>
<a id="link3" href="http://www.google.com">Link #3</a>
<a id="link4" href="http://www.google.com" style="visibility:collapse">Link #4</a>
<a id="link5" href="http://www.google.com">Link #5</a>
<div id="testContainer">
<a id="link1" href="http://www.google.com">Link #1</a>
<a id="link2" href="http://www.google.com">Link #2</a>
<a id="link3" href="http://www.google.com">Link #3</a>
<a id="link4" href="http://www.google.com" style="visibility:collapse">Link #4</a>
<a id="link5" href="http://www.google.com">Link #5</a>
<div id="container" role="list"><span id="child1"></span><span id="child2" role="listitem"></span></div>
<div id="container" role="list">
<span id="child1"></span>
<span id="child2" role="listitem"></span>
<span id="child3"><span role="listitem"></span></span>
</div>
<a id="link6" href="http://www.google.com">Link #6</a>
<a id="link6" href="http://www.google.com">Link #6</a>
<div id="container2" class="displayNone"><a id="link7">Link #7</a></div>
<div id="container3" class="visibilityHidden"><a id="link8">Link #8</a></div>
<div id="container2" class="displayNone"><a id="link7">Link #7</a></div>
<div id="container3" class="visibilityHidden"><a id="link8">Link #8</a></div>
</div>
</body>
</html>