1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/domplate",
  7     "firebug/lib/locale",
  8     "firebug/lib/events",
  9     "firebug/lib/url",
 10     "firebug/lib/css",
 11     "firebug/lib/dom",
 12     "firebug/lib/xml",
 13     "firebug/lib/xpath",
 14     "firebug/console/console",
 15     "firebug/chrome/infotip",
 16 ],
 17 function(Obj, Firebug, Domplate, Locale, Events, Url, Css, Dom, Xml, Xpath) {
 18 
 19 // ************************************************************************************************
 20 // Constants
 21 
 22 var singleSpaceTag = Domplate.DIV({"class" : "a11y1emSize"}, "x");
 23 
 24 var KeyEvent = window.KeyEvent;
 25 
 26 // ************************************************************************************************
 27 // Module Management
 28 
 29 Firebug.A11yModel = Obj.extend(Firebug.Module,
 30 {
 31     dispatchName: "a11y",
 32 
 33     initialize: function()
 34     {
 35         Firebug.Module.initialize.apply(this, arguments);
 36 
 37         this.handleTabBarFocus = Obj.bind(this.handleTabBarFocus, this);
 38         this.handleTabBarBlur = Obj.bind(this.handleTabBarBlur, this);
 39         this.handlePanelBarKeyPress = Obj.bind(this.handlePanelBarKeyPress, this);
 40         this.onNavigablePanelKeyPress = Obj.bind(this.onNavigablePanelKeyPress, this);
 41         this.onConsoleMouseDown = Obj.bind(this.onConsoleMouseDown, this);
 42         this.onLayoutKeyPress = Obj.bind(this.onLayoutKeyPress, this);
 43         this.onCSSKeyPress = Obj.bind(this.onCSSKeyPress, this);
 44         this.onCSSMouseDown = Obj.bind(this.onCSSMouseDown, this);
 45         this.onHTMLKeyPress = Obj.bind(this.onHTMLKeyPress, this);
 46         this.onHTMLFocus = Obj.bind(this.onHTMLFocus, this);
 47         this.onHTMLBlur = Obj.bind(this.onHTMLBlur, this);
 48         this.onPanelFocus = Obj.bind(this.onPanelFocus, this);
 49         this.onLayoutFocus = Obj.bind(this.onLayoutFocus, this);
 50         this.onLayoutBlur = Obj.bind(this.onLayoutBlur, this);
 51         this.onScriptContextMenu = Obj.bind(this.onScriptContextMenu, this);
 52         this.onCSSPanelContextMenu = Obj.bind(this.onCSSPanelContextMenu, this);
 53         this.onScriptKeyPress = Obj.bind(this.onScriptKeyPress, this);
 54         this.onScriptKeyUp = Obj.bind(this.onScriptKeyUp, this);
 55         this.onScriptMouseUp = Obj.bind(this.onScriptMouseUp, this);
 56         this.onNetMouseDown = Obj.bind(this.onNetMouseDown, this);
 57         this.onNetFocus = Obj.bind(this.onNetFocus, this);
 58         this.onNetBlur = Obj.bind(this.onNetBlur, this);
 59 
 60         // Mark ourselves disabled, so we don't performDisable() if we are not enabled.
 61         Firebug.chrome.window.a11yEnabled = false;
 62 
 63         Firebug.connection.addListener(this);
 64         Firebug.Console.addListener(this);
 65         Firebug.DOMModule.addListener(this);
 66     },
 67 
 68     shutdown: function()
 69     {
 70         Firebug.connection.removeListener(this);
 71         Firebug.Console.removeListener(this);
 72         Firebug.DOMModule.removeListener(this);
 73 
 74         Firebug.Module.shutdown.apply(this, arguments);
 75     },
 76 
 77     initializeUI: function()
 78     {
 79         //Initialize according to the current pref value.
 80         this.updateOption("a11y.enable", this.isEnabled());
 81     },
 82 
 83     isEnabled: function()
 84     {
 85         return Firebug.Options.get("a11y.enable");
 86     },
 87 
 88     updateOption: function(name, value)
 89     {
 90         if (FBTrace.DBG_A11Y)
 91         {
 92             FBTrace.sysout("a11y.updateOption; " + name + ": " + value +
 93                 ", Current chrome: " + Firebug.chrome.getName() +
 94                 ", Original chrome: " + Firebug.originalChrome.getName());
 95         }
 96 
 97         if (name == "a11y.enable")
 98         {
 99             // Update for current chrome
100             this.set(value, Firebug.chrome);
101             // If the current chrome is an external window, update also the original chrome.
102             if (Firebug.chrome != Firebug.originalChrome)
103             {
104                 this.set(value, Firebug.originalChrome);
105                 if (FBTrace.DBG_A11Y)
106                     FBTrace.sysout("a11y.updateOption; (original chrome)");
107             }
108         }
109     },
110 
111     set: function(enable, chrome)
112     {
113         if (chrome.window.a11yEnabled == enable)
114             return;
115 
116         if (enable)
117             this.performEnable(chrome);
118         else
119             this.performDisable(chrome);
120         chrome.window.a11yEnabled = enable;
121     },
122 
123     performEnable: function(chrome)
124     {
125         var tmpElem;
126         //add class used by all a11y related css styles (e.g. :focus and -moz-user-focus styles)
127         Css.setClass(chrome.$("fbContentBox"), "useA11y");
128         //Css.setClass(chrome.$("fbStatusBar"), "useA11y");
129         tmpElem = chrome.$("fbStatusPrefix");
130         if (tmpElem) tmpElem.setAttribute("value", Locale.$STR("a11y.labels.firebug status"));
131 
132         //manage all key events in toolbox (including tablists)
133         tmpElem = chrome.$("fbContentBox");
134         if (tmpElem)
135             Events.addEventListener(tmpElem, "keypress", this.handlePanelBarKeyPress, true);
136 
137         //make focus stick to inspect button when clicked
138         tmpElem = chrome.$("fbInspectButton");
139         if (tmpElem)
140             Events.addEventListener(tmpElem, "mousedown", this.focusTarget, true);
141 
142         tmpElem = chrome.$("fbPanelBar1-panelTabs");
143         if (tmpElem)
144             Events.addEventListener(tmpElem, "focus", this.handleTabBarFocus, true);
145 
146         tmpElem = chrome.$("fbPanelBar1-panelTabs");
147         if (tmpElem)
148             Events.addEventListener(tmpElem, "blur", this.handleTabBarBlur, true);
149 
150         tmpElem = chrome.$("fbPanelBar2-panelTabs");
151         if (tmpElem)
152             Events.addEventListener(tmpElem, "focus", this.handleTabBarFocus, true);
153 
154         tmpElem = chrome.$("fbPanelBar2-panelTabs");
155         if (tmpElem)
156             Events.addEventListener(tmpElem, "blur", this.handleTabBarBlur, true);
157 
158         tmpElem = chrome.$("fbPanelBar1");
159         if (tmpElem)
160             Css.setClass(tmpElem.browser.contentDocument.body, "useA11y");
161 
162         tmpElem = chrome.$("fbPanelBar2");
163         if (tmpElem)
164             Css.setClass(tmpElem.browser.contentDocument.body, "useA11y");
165         Firebug.Editor.addListener(this);
166         this.listeningToEditor = true;
167     },
168 
169     performDisable: function(chrome)
170     {
171         var tmpElem;
172         //undo everything we did in performEnable
173         Css.removeClass(chrome.$("fbContentBox"), "useA11y");
174         Css.removeClass(chrome.$("fbStatusBar"), "useA11y");
175 
176         tmpElem = chrome.$("fbPanelBar1");
177         if (tmpElem)
178             Events.removeEventListener(tmpElem, "keypress", this.handlePanelBarKeyPress, true);
179 
180         tmpElem = chrome.$("fbInspectButton");
181         if (tmpElem)
182             Events.removeEventListener(tmpElem, "mousedown", this.focusTarget, true);
183 
184         tmpElem = chrome.$("fbPanelBar1-panelTabs");
185         if (tmpElem)
186             Events.removeEventListener(tmpElem, "focus", this.handleTabBarFocus, true);
187 
188         tmpElem = chrome.$("fbPanelBar1-panelTabs")
189         if (tmpElem)
190             Events.removeEventListener(tmpElem, "blur", this.handleTabBarBlur, true);
191 
192         tmpElem = chrome.$("fbPanelBar2-panelTabs");
193         if (tmpElem)
194             Events.removeEventListener(tmpElem, "focus", this.handleTabBarFocus, true);
195 
196         tmpElem = chrome.$("fbPanelBar2-panelTabs");
197         if (tmpElem)
198             Events.removeEventListener(tmpElem, "blur", this.handleTabBarBlur, true);
199 
200         tmpElem = chrome.$("fbPanelBar1");
201         if (tmpElem)
202         {
203             Css.removeClass(tmpElem.browser.contentDocument.body, "useA11y");
204             tmpElem.browser.setAttribute("showcaret", false);
205         }
206 
207         tmpElem = chrome.$("fbPanelBar2");
208         if (tmpElem)
209             Css.removeClass(tmpElem.browser.contentDocument.body, "useA11y");
210 
211         if (this.listeningToEditor)
212             Firebug.Editor.removeListener(this);
213     },
214 
215     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
216 
217     onCreatePanel: function(context, panel, panelType)
218     {
219         if (!panel.enableA11y)
220             return;
221 
222         if (panel.addListener)
223             panel.addListener(this);
224     },
225 
226     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
227     // Context & Panel Management
228 
229     onInitializeNode: function(panel)
230     {
231         var panelA11y = this.getPanelA11y(panel, true);
232         if (!panelA11y)
233             return;
234 
235         panelA11y.tabStop = null;
236         panelA11y.manageFocus = false;
237         panelA11y.lastIsDefault = false;
238         panelA11y.type = panel.deriveA11yFrom ? panel.deriveA11yFrom : panel.name;
239 
240         //panel.context.chrome.$("fbContentBox").addEventListener("focus", this.reportFocus, true);
241         this.makeFocusable(panel.panelNode, false);
242 
243         switch (panelA11y.type)
244         {
245             case "console":
246                 panelA11y.manageFocus = true;
247                 switch (panel.name)
248                 {
249                     case "console":
250                         panel.panelNode.setAttribute("aria-label",
251                             Locale.$STR("a11y.labels.log rows"));
252                         panelA11y.lastIsDefault = true;
253                         panel.panelNode.setAttribute("role", "list");
254                         break;
255 
256                     case "callstack":
257                         panel.panelNode.setAttribute("role", "list");
258                         panel.panelNode.setAttribute("aria-label",
259                             Locale.$STR("a11y.labels.call stack"));
260                         break;
261 
262                     default:
263                         panel.panelNode.setAttribute("role", "presentation");
264                 }
265                 panel.panelNode.setAttribute("aria-live", "polite");
266                 panel.panelNode.setAttribute("aria-relevant", "additions");
267                 Events.addEventListener(panel.panelNode, "keypress", this.onNavigablePanelKeyPress, false);
268                 Events.addEventListener(panel.panelNode, "focus", this.onPanelFocus, true);
269                 Events.addEventListener(panel.panelNode, "mousedown", this.onConsoleMouseDown, false);
270                 if (panel.name == "breakpoints")
271                     panel.panelNode.style.overflowX = "hidden";
272                 break;
273 
274             case "html":
275                 panel.panelNode.setAttribute("role", "tree");
276                 panel.panelNode.setAttribute("aria-label",
277                     Locale.$STR("a11y.labels.document structure"));
278                 Events.addEventListener(panel.panelNode, "keypress", this.onHTMLKeyPress, false);
279                 Events.addEventListener(panel.panelNode, "focus", this.onHTMLFocus, true);
280                 Events.addEventListener(panel.panelNode, "blur", this.onHTMLBlur, true);
281                 break;
282 
283             case "css":
284                 panelA11y.manageFocus = true;
285                 Events.addEventListener(panel.panelNode, "keypress", this.onCSSKeyPress, false);
286                 Events.addEventListener(panel.panelNode, "mousedown", this.onCSSMouseDown, false);
287                 Events.addEventListener(panel.panelNode, "focus", this.onPanelFocus, true);
288                 Events.addEventListener(panel.panelNode, "contextmenu", this.onCSSPanelContextMenu, false)
289                 this.insertHiddenText(panel, panel.panelNode,
290                     Locale.$STR("a11y.labels.overridden"), false, "CSSOverriddenDescription");
291                 panel.panelNode.setAttribute("role", panel.name == "stylesheet" ?
292                     "list" : "presentation");
293                 break;
294 
295             case "layout":
296                 panelA11y.manageFocus = true;
297                 Events.addEventListener(panel.panelNode, "keypress", this.onLayoutKeyPress, false);
298                 Events.addEventListener(panel.panelNode, "focus", this.onLayoutFocus, true);
299                 Events.addEventListener(panel.panelNode, "blur", this.onLayoutBlur, true);
300                 break;
301 
302             case "script":
303                 Events.addEventListener(panel.panelNode, "contextmenu", this.onScriptContextMenu, true);
304                 Events.addEventListener(panel.panelNode, "keypress", this.onScriptKeyPress, true);
305                 Events.addEventListener(panel.panelNode, "keyup", this.onScriptKeyUp, true);
306                 Events.addEventListener(panel.panelNode, "mouseup", this.onScriptMouseUp, true);
307                 panelA11y.oneEmElem = this.addSingleSpaceElem(panel.panelNode);
308                 break;
309 
310             case "net":
311                 panelA11y.manageFocus = true;
312                 Events.addEventListener(panel.panelNode, "keypress", this.onNavigablePanelKeyPress, false);
313                 Events.addEventListener(panel.panelNode, "focus", this.onPanelFocus, true);
314                 Events.addEventListener(panel.panelNode, "focus", this.onNetFocus, true);
315                 Events.addEventListener(panel.panelNode, "blur", this.onNetBlur, true);
316                 Events.addEventListener(panel.panelNode, "mousedown", this.onNetMouseDown, false);
317                 break;
318         }
319     },
320 
321     onDestroyNode: function(panel)
322     {
323         var panelA11y = this.getPanelA11y(panel);
324         if (!panelA11y)
325             return;
326 
327         panelA11y = null;
328 
329         // Remove all event handlers we added in onInitializeNode.
330         var actAsPanel = panel.deriveA11yFrom ? panel.deriveA11yFrom : panel.name;
331         switch (actAsPanel)
332         {
333             case "console":
334                 Events.removeEventListener(panel.panelNode, "keypress", this.onNavigablePanelKeyPress,
335                     false);
336                 Events.removeEventListener(panel.panelNode, "focus", this.onPanelFocus, true);
337                 Events.removeEventListener(panel.panelNode, "mousedown", this.onConsoleMouseDown, false);
338                 break;
339 
340             case "html":
341                 Events.removeEventListener(panel.panelNode, "keypress", this.onHTMLKeyPress, false);
342                 Events.removeEventListener(panel.panelNode, "focus", this.onHTMLFocus, true);
343                 Events.removeEventListener(panel.panelNode, "blur", this.onHTMLBlur, true);
344                 break;
345 
346             case "css":
347                 Events.removeEventListener(panel.panelNode, "keypress", this.onCSSKeyPress, false);
348                 Events.removeEventListener(panel.panelNode, "mousedown", this.onCSSMouseDown, false);
349                 Events.removeEventListener(panel.panelNode, "focus", this.onPanelFocus, true);
350                 Events.removeEventListener(panel.panelNode, "blur", this.onPanelBlur, true);
351                 Events.removeEventListener(panel.panelNode, "contextmenu", this.onCSSPanelContextMenu,
352                     false)
353                 break;
354 
355             case "layout":
356                 Events.removeEventListener(panel.panelNode, "keypress", this.onLayoutKeyPress, false);
357                 Events.removeEventListener(panel.panelNode, "focus", this.onLayoutFocus, true);
358                 Events.removeEventListener(panel.panelNode, "blur", this.onLayoutBlur, true);
359                 break;
360 
361             case "script":
362                 Events.removeEventListener(panel.panelNode, "contextmenu", this.onScriptContextMenu, true);
363                 Events.removeEventListener(panel.panelNode, "keypress", this.onScriptKeyPress, true);
364                 Events.removeEventListener(panel.panelNode, "keyup", this.onScriptKeyUp, true);
365                 Events.removeEventListener(panel.panelNode, "mouseup", this.onScriptMouseUp, true)
366                 break;
367 
368             case "net":
369                 Events.removeEventListener(panel.panelNode, "keypress", this.onNavigablePanelKeyPress,
370                     false);
371                 Events.removeEventListener(panel.panelNode, "focus", this.onPanelFocus, true);
372                 Events.removeEventListener(panel.panelNode, "focus", this.onNetFocus, true);
373                 Events.removeEventListener(panel.panelNode, "blur", this.onNetBlur, true);
374                 Events.removeEventListener(panel.panelNode, "mousedown", this.onNetMouseDown, false);
375                 break;
376         }
377     },
378 
379     showPanel: function(browser, panel)
380     {
381         var panelA11y = this.getPanelA11y(panel);
382         if (!panelA11y)
383             return;
384         var title = panel.name;
385         var panelType = Firebug.getPanelType(panel.name);
386         if (panelType)
387             title = Firebug.getPanelTitle(panelType);
388         Firebug.chrome.$("fbToolbar").setAttribute("aria-label", title + " " +
389             Locale.$STR("a11y.labels.panel tools"))
390         var panelBrowser = Firebug.chrome.getPanelBrowser(panel);
391         panelBrowser.setAttribute("showcaret", (panel.name == "script"));
392         panelBrowser.contentDocument.body.setAttribute("aria-label",
393             Locale.$STRF("a11y.labels.title panel", [title]));
394     },
395 
396     showSidePanel: function(browser, sidePanel)
397     {
398         var panelA11y = this.getPanelA11y(sidePanel);
399         if (!panelA11y)
400             return;
401         var panelBrowser = Firebug.chrome.getPanelBrowser(sidePanel);
402         var panelType = Firebug.getPanelType(sidePanel.name);
403         if (panelType)
404             title = Firebug.getPanelTitle(panelType);
405         panelBrowser.contentDocument.body.setAttribute("aria-label",
406             Locale.$STRF("a11y.labels.title side panel", [title]));
407     },
408 
409     addLiveElem: function(panel, role, politeness)
410     {
411         var panelA11y = this.getPanelA11y(panel);
412         if (!panelA11y)
413             return;
414         if (panelA11y.liveElem && Dom.isElement(panelA11y.liveElem))
415             return;
416 
417         var attrName = attrValue = "";
418         if (role)
419         {
420             attrName = "role";
421             attrValue = role;
422         }
423         else
424         {
425             attrName = "aria-live";
426             attrValue = politeness ? politeness : "polite";
427         }
428         var elem = panel.document.createElement("div");
429         elem.setAttribute(attrName, attrValue);
430         elem.className = "offScreen";
431         panel.document.body.appendChild(elem);
432         panelA11y.liveElem = elem;
433         return elem;
434     },
435 
436     updateLiveElem: function(panel, msg, useAlert)
437     {
438         var panelA11y = this.getPanelA11y(panel);
439         if (!panelA11y)
440             return;
441         var elem = panelA11y.liveElem;
442         if (!elem)
443             elem = this.addLiveElem(panel);
444         elem.textContent = msg;
445         if (useAlert)
446             elem.setAttribute("role", "alert");
447     },
448 
449     addSingleSpaceElem: function(parent)
450     {
451         return singleSpaceTag.append({}, parent, this);
452     },
453 
454     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
455     // Toolbars & Tablists
456 
457     focusTarget: function(event)
458     {
459         this.focus(event.target);
460     },
461 
462     handlePanelBarKeyPress: function (event)
463     {
464         var target = event.originalTarget;
465         var isTab = target.nodeName.toLowerCase() == "paneltab";
466         var isButton = target.nodeName.search(/(xul:)?((toolbar)?button)|(checkbox)/) != -1;
467         var isDropDownMenu = isButton && (target.getAttribute("type") == "menu" ||
468             target.id == "fbLocationList");
469         var siblingTab, forward, toolbar, buttons;
470         var keyCode = event.keyCode || (event.type == "keypress" ? event.charCode : null);
471         if (keyCode == KeyEvent.DOM_VK_TAB)
472             this.ensurePanelTabStops(); //TODO: need a better solution to prevent loss of panel tabstop
473         if (isTab || isButton)
474         {
475             switch (keyCode)
476             {
477                 case KeyEvent.DOM_VK_LEFT:
478                 case KeyEvent.DOM_VK_RIGHT:
479                 case KeyEvent.DOM_VK_UP:
480                 case KeyEvent.DOM_VK_DOWN:
481                     forward = (event.keyCode == KeyEvent.DOM_VK_RIGHT ||
482                         event.keyCode == KeyEvent.DOM_VK_DOWN);
483                     if (isTab)
484                     {
485                         //will only work as long as long as siblings only consist of paneltab elements
486                         siblingTab = target[forward ? "nextSibling" : "previousSibling"];
487                         if (!siblingTab)
488                             siblingTab = target.parentNode[forward ? "firstChild" : "lastChild"];
489                         if (siblingTab)
490                         {
491                             var panelBar = Dom.getAncestorByClass(target, "panelBar");
492                             setTimeout(Obj.bindFixed(function()
493                             {
494                                 panelBar.selectTab(siblingTab);
495                                 this.focus(siblingTab);
496                             }, this));
497                         }
498                     }
499                     else if (isButton)
500                     {
501                         if (target.id == "fbFirebugMenu" && !forward)
502                         {
503                              Events.cancelEvent(event);
504                              return;
505                         }
506                         toolbar = Dom.getAncestorByClass(target, "innerToolbar");
507                         if (toolbar)
508                         {
509                             var doc = target.ownerDocument;
510                             //temporarily make all buttons in the toolbar part of the tab order,
511                             //to allow smooth, native focus advancement
512                             Css.setClass(toolbar, "hasTabOrder");
513                             setTimeout(Obj.bindFixed(function() // time out needed to fix this behavior in 3.6
514                             {
515                                 doc.commandDispatcher[forward ? "advanceFocus" : "rewindFocus"]();
516                                 //remove the buttons from the tab order again, so that it will remain uncluttered
517                                 //Very ugly hack, but it works well. This prevents focus to 'spill out' of a
518                                 //toolbar when using the left and right arrow keys
519                                 if (!Dom.isAncestor(doc.commandDispatcher.focusedElement, toolbar))
520                                 {
521                                     //we moved focus to somewhere out of the toolbar: not good. Move it back to where it was.
522                                     doc.commandDispatcher[!forward ?
523                                         "advanceFocus" : "rewindFocus"]();
524                                 }
525                                 Css.removeClass(toolbar, "hasTabOrder");
526                             }, this));
527                         }
528                         Events.cancelEvent(event);
529                         return;
530                     }
531                 break;
532  
533                 case KeyEvent.DOM_VK_RETURN:
534                 case KeyEvent.DOM_VK_SPACE:
535                     if (isTab && target.tabMenu)
536                     {
537                         target.tabMenu.popup.showPopup(target.tabMenu, -1, -1, "popup",
538                             "bottomleft", "topleft");
539                     }
540                     else if (isButton && isDropDownMenu)
541                     {
542                         if (target.id == "fbLocationList")
543                             target.showPopup();
544                         else
545                             target.open = true;
546                         Events.cancelEvent(event);
547                         return false;
548                     }
549                 break;
550 
551                 case KeyEvent.DOM_VK_F4:
552                     if (isTab && target.tabMenu)
553                     {
554                         target.tabMenu.popup.showPopup(target.tabMenu, -1, -1, "popup",
555                             "bottomleft", "topleft");
556                     }
557                 break;
558             }
559         }
560     },
561 
562     handleTabBarFocus: function(event)
563     {
564         this.tabFocused = true;
565     },
566 
567     handleTabBarBlur: function(event)
568     {
569         this.tabFocused = false;
570     },
571 
572     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
573     // Panel Focus & Tab Order Management
574 
575     getPanelTabStop: function(panel)
576     {
577         var panelA11y = this.getPanelA11y(panel);
578         if (panelA11y)
579             return panelA11y.tabStop;
580         if (FBTrace.DBG_ERRORS)
581             FBTrace.sysout("a11y.getPanelTabStop null panel.context");
582         return null;
583     },
584 
585     ensurePanelTabStops: function()
586     {
587         // XXXjjb: seems like this should be !Firebug.chrome
588         if (!Firebug.currentContext || !Firebug.currentContext.chrome)
589             return;
590         var panel = Firebug.chrome.getSelectedPanel();
591         var sidePanel = Firebug.chrome.getSelectedSidePanel();
592         this.ensurePanelTabStop(panel);
593         if (sidePanel)
594             this.ensurePanelTabStop(sidePanel);
595     },
596 
597     ensurePanelTabStop: function(panel)
598     {
599         var panelA11y = this.getPanelA11y(panel);
600         if (!panelA11y)
601             return;
602 
603         if (panelA11y.manageFocus)
604         {
605             var tabStop = this.getPanelTabStop(panel);
606             if (!tabStop || !this.isVisibleByStyle(tabStop) || !Xml.isVisible(tabStop))
607             {
608                 this.tabStop = null;
609                 this.findPanelTabStop(panel, "focusRow", panelA11y.lastIsDefault);
610             }
611             else if (tabStop.getAttribute("tabindex") !== "0")
612             {
613                 tabStop.setAttribute("tabindex", "0");
614             }
615 
616             if (tabStop)
617                 this.checkModifiedState(panel, tabStop, true);
618         }
619     },
620 
621     checkModifiedState: function(panel, elem, makeTab)
622     {
623         var panelA11y = this.getPanelA11y(panel);
624         if (!panelA11y || !elem)
625             return;
626 
627         if (panelA11y.type == "console" && Css.hasClass(elem, "focusRow"))
628             this.modifyPanelRow(panel, elem, makeTab);
629     },
630 
631     setPanelTabStop: function (panel, elem)
632     {
633         var panelA11y = this.getPanelA11y(panel);
634         if (!panelA11y)
635             return;
636 
637         var tabStop = this.getPanelTabStop(panel)
638         if (tabStop)
639         {
640             this.makeFocusable(tabStop, false);
641             if (["treeitem", "listitem", "option"].indexOf(tabStop.getAttribute("role")) != -1)
642                 tabStop.setAttribute("aria-selected", "false");
643         }
644         panelA11y.tabStop = elem;
645         if (elem)
646         {
647             panelA11y.reFocusId = null;
648             this.makeFocusable(elem, true);
649             if (["treeitem", "listitem", "option"].indexOf(elem.getAttribute("role")) != -1)
650                 elem.setAttribute("aria-selected", "true");
651         }
652     },
653 
654     findPanelTabStop: function(panel, className, last)
655     {
656         var candidates = panel.panelNode.getElementsByClassName(className);
657         candidates= Array.filter(candidates, function(e, i, a){return this.isVisibleByStyle(e)
658           && Xml.isVisible(e);}, this);
659         if (candidates.length > 0)
660         {
661             var chosenRow = candidates[last ? candidates.length -1 : 0];
662             this.modifyPanelRow(panel, chosenRow, true)
663             this.setPanelTabStop(panel, chosenRow);
664         }
665         else
666         {
667             this.setPanelTabStop(panel, null);
668         }
669     },
670 
671     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
672     // Console Panel
673 
674     onLogRowCreated: function(panel, row)
675     {
676         var panelA11y = this.getPanelA11y(panel);
677         if (!panelA11y)
678             return;
679 
680         if (Css.hasClass(row, "logRow-dir"))
681         {
682             row.setAttribute("role", "listitem");
683             Css.setClass(row, "outerFocusRow");
684             var memberRows = row.getElementsByClassName("memberRow");
685             if (memberRows.length > 0)
686                 this.onMemberRowsAdded(panel, memberRows);
687         }
688         else if (Css.hasClass(row, "logRow-group") || Css.hasClass(row, "logRow-profile"))
689         {
690             row.setAttribute("role", "presentation");
691             var focusRow = row.getElementsByClassName("logGroupLabel").item(0);
692             if (focusRow)
693             {
694                 this.setPanelTabStop(panel, focusRow);
695                 focusRow.setAttribute("aria-expanded", Css.hasClass(row, "opened") + "");
696                 if (!Css.hasClass(row, "logRow-profile"))
697                     this.insertHiddenText(panel, focusRow, "group label: ");
698             }
699         }
700         else if (Css.hasClass(row, "logRow-errorMessage") || Css.hasClass(row,
701             "logRow-warningMessage"))
702         {
703             Css.setClass(row, "outerFocusRow");
704             row.setAttribute("role", "presentation");
705             var focusRow = row.getElementsByClassName("errorTitle").item(0);
706             if (focusRow)
707             {
708                 this.setPanelTabStop(panel, focusRow);
709                 focusRow.setAttribute("aria-expanded",
710                     Css.hasClass(focusRow.parentNode, "opened") + "");
711             }
712         }
713         else if (Css.hasClass(row, "logRow-stackTrace"))
714         {
715             Css.setClass(row, "outerFocusRow");
716             row.setAttribute("role", "listitem");
717             var stackFrames = row.getElementsByClassName("focusRow");
718             Array.forEach(stackFrames, function(e, i, a)
719             {
720                 e.setAttribute("role", "listitem");
721                 if ((panelA11y.lastIsDefault && i === stackFrames.length - 1) ||
722                     (!panelA11y.lastIsDefault && i === 0))
723                 {
724                     this.setPanelTabStop(panel, e);
725                 }
726                 else
727                 {
728                     this.makeFocusable(e, false);
729                 }
730             }, this);
731         }
732         else if (Css.hasClass(row, "logRow-spy"))
733         {
734             var focusRow = Dom.getChildByClass(row, "spyHeadTable");
735             if (focusRow)
736                 this.makeFocusable(focusRow, true);
737         }
738         else
739         {
740             row.setAttribute("role", "listitem");
741             Css.setClass(row, "focusRow");
742             Css.setClass(row, "outerFocusRow");
743             if (Xml.isVisible(row))
744                 this.setPanelTabStop(panel, row);
745         }
746     },
747 
748     modifyLogRow: function(panel, row, inTabOrder)
749     {
750         this.makeFocusable(row, inTabOrder);
751         var logRowType = this.getLogRowType(row);
752         if (logRowType)
753             this.insertHiddenText(panel, row, logRowType + ": ");
754         var arrayNode = Dom.getChildByClass(row, "objectBox-array");
755         if (arrayNode)
756         {
757             arrayNode.setAttribute("role", "group");
758             this.insertHiddenText(panel, row, "array" + ": ");
759         }
760         var focusObjects = this.getFocusObjects(row);
761         Array.forEach(focusObjects, function(e, i, a)
762         {
763             this.makeFocusable(e);
764             var prepend = "";
765             var append = " (" + this.getObjectType(e) + ") ";
766             if (e.textContent != "")
767                 e.setAttribute("aria-label", prepend + e.textContent + append);
768             if (arrayNode)
769                 e.setAttribute("role", "listitem");
770         }, this);
771     },
772 
773     onNavigablePanelKeyPress: function(event)
774     {
775         var target = event.target;
776         var keyCode = event.keyCode || (event.type == "keypress" ? event.charCode : null);
777         if (!this.isTabWorthy(target) && !this.isFocusNoTabObject(target))
778             return;
779         else if (event.shiftKey || event.altKey)
780             return;
781         else if ([13, 32, 33, 34, 35, 36, 37, 38, 39, 40, 46].indexOf(keyCode) == -1)
782             return; //not interested in any other keys, than arrows, pg, home/end, del space & enter
783         var panel = Firebug.getElementPanel(target)
784         var panelA11y = this.getPanelA11y(panel);
785         if (!panelA11y)
786             return;
787 
788         var newTarget = target
789         if (!this.isOuterFocusRow(target))
790         {
791             if (Events.isControl(event))
792             {
793                 newTarget = this.getAncestorRow(target);
794                 if (newTarget)
795                 {
796                     newTarget = [33, 38].indexOf(keyCode) == -1 ?
797                         this.getLastFocusChild(newTarget) : this.getFirstFocusChild(newTarget);
798                 }
799             }
800             else if (!this.isDirCell(target) || Css.hasClass(target, "netInfoTab") ||
801                 Css.hasClass(target, "netCol"))
802             {
803                 newTarget = this.getAncestorRow(target, true);
804             }
805 
806             if (!newTarget)
807                 newTarget = target;
808         }
809         switch (keyCode)
810         {
811             case KeyEvent.DOM_VK_UP:
812             case KeyEvent.DOM_VK_DOWN:
813                 if (!this.isFocusNoTabObject(target))
814                 {
815                     this.focusSiblingRow(panel, newTarget, keyCode == 38);
816                     Events.cancelEvent(event);
817                 }
818                 break;
819 
820             case KeyEvent.DOM_VK_LEFT:
821             case KeyEvent.DOM_VK_RIGHT:
822                 var goLeft = keyCode == KeyEvent.DOM_VK_LEFT;
823                 if (this.isDirCell(target))
824                 {
825                     var row = Dom.getAncestorByClass(target, "memberRow");
826                     var toggleElem = Dom.getChildByClass(row.cells[1], "memberLabel");
827                     if (!goLeft && Css.hasClass(row, "hasChildren"))
828                     {
829                         if (Css.hasClass(row, "opened"))
830                         {
831                             this.focusSiblingRow(panel, target, false);
832                         }
833                         else if (toggleElem)
834                         {
835                             if (Css.hasClass(row, "hasChildren"))
836                                 target.setAttribute("aria-expanded", "true");
837                             this.dispatchMouseEvent(toggleElem, "click");
838                         }
839                     }
840                     else if (goLeft)
841                     {
842                         var level = parseInt(row.getAttribute("level"));
843                         if (Css.hasClass(row, "opened"))
844                         {
845                             if (Css.hasClass(row, "hasChildren"))
846                                 target.setAttribute("aria-expanded", "false");
847                             this.dispatchMouseEvent(toggleElem, "click");
848                         }
849                         else if (level > 0)
850                         {
851                             var targetLevel = (level - 1) + "";
852                             var newRows = Array.filter(row.parentNode.rows, function(e, i, a)
853                             {
854                                 return e.rowIndex < row.rowIndex &&
855                                     e.getAttribute("level") == targetLevel;
856                             }, this);
857 
858                             if (newRows.length)
859                                 this.focus(newRows[newRows.length -1].cells[2].firstChild);
860                         }
861                     }
862                     Events.cancelEvent(event);
863                 }
864                 else if (this.isOuterFocusRow(target, true))
865                 {
866                     if (target.hasAttribute("aria-expanded"))
867                     {
868                         if (target.getAttribute("role") == "row" ||
869                             Css.hasClass(target, "spyHeadTable"))
870                         {
871                             if (goLeft && target.getAttribute("aria-expanded") == "true")
872                             {
873                                 var toggleElem = Css.hasClass(target, "spyHeadTable") ?
874                                     target.getElementsByClassName("spyTitleCol").item(0) : target;
875                                 if (toggleElem)
876                                     this.dispatchMouseEvent(toggleElem, "click");
877                             }
878                         }
879                         else if (target.getAttribute("aria-expanded") == (goLeft ?
880                             "true" : "false"))
881                         {
882                             this.dispatchMouseEvent(target, Css.hasClass(target, "logGroupLabel") ?
883                                 "mousedown" : "click");
884                         }
885                     }
886                     if (goLeft)
887                     {
888                         //check if we"re in an expanded section
889                         var inExpanded = false, groupClass, groupLabelClass, group, groupLabel;
890                         if (Css.hasClass(target, "objectBox-stackFrame"))
891                         {
892                             inExpanded = true;
893                             groupClass = "errorTrace";
894                             groupLabelClass = "errorTitle";
895                         }
896                         else if (Dom.getAncestorByClass(target, "logGroupBody"))
897                         {
898                             inExpanded = true;
899                             groupClass = "logGroupBody";
900                             groupLabelClass = "logGroupLabel";
901                         }
902                         if (inExpanded)
903                         {
904                             group = Dom.getAncestorByClass(target, groupClass);
905                             if (group)
906                             {
907                                 groupLabel = this.getPreviousByClass(target, groupLabelClass,
908                                     false, panel.panelNode);
909                                 if (groupLabel)
910                                 {
911                                     this.modifyPanelRow(panel, groupLabel);
912                                     this.focus(groupLabel);
913                                 }
914                             }
915                         }
916                     }
917                     else if (!goLeft)
918                     {
919     
920                         var focusItems = this.getFocusObjects(target);
921                         if (focusItems.length > 0)
922                         {
923                             this.focus(Events.isControl(event) ?
924                                 focusItems[focusItems.length -1] : focusItems[0]);
925                         }
926                     }
927                 }
928                 else if (this.isFocusObject(target))
929                 {
930                     var parentRow = this.getAncestorRow(target, true);
931                     var focusObjects = this.getFocusObjects(parentRow);
932                     if (!Events.isControl(event))
933                     {
934                         var focusIndex = Array.indexOf(focusObjects, target);
935                         var newIndex = goLeft ? --focusIndex : ++focusIndex;
936                         this.focus(goLeft && newIndex < 0 ? parentRow : focusObjects[newIndex]);
937                     }
938                     else
939                     {
940                         this.focus(goLeft ? parentRow : focusObjects[focusObjects.length -1]);
941                     }
942                     Events.cancelEvent(event);
943                 }
944                 break;
945 
946             case KeyEvent.DOM_VK_END:
947             case KeyEvent.DOM_VK_HOME:
948                 this.focusEdgeRow(panel, newTarget, keyCode == KeyEvent.DOM_VK_HOME);
949                 Events.cancelEvent(event);
950                 break;
951 
952             case KeyEvent.DOM_VK_PAGE_UP:
953             case KeyEvent.DOM_VK_PAGE_DOWN:
954                 this.focusPageSiblingRow(panel, newTarget, keyCode == KeyEvent.DOM_VK_PAGE_UP);
955                 Events.cancelEvent(event);
956                 break;
957 
958             case KeyEvent.DOM_VK_RETURN:
959                 if (this.isFocusObject(target))
960                 {
961                     this.dispatchMouseEvent(target, "click");
962                 }
963                 else if (Css.hasClass(target, "watchEditBox"))
964                 {
965                     this.dispatchMouseEvent(target, "mousedown");
966                     Events.cancelEvent(event);
967                 }
968                 else if (Css.hasClass(target, "breakpointRow"))
969                 {
970                     var sourceLink =
971                         target.getElementsByClassName("objectLink-sourceLink").item(0);
972                     if (sourceLink)
973                         this.dispatchMouseEvent(sourceLink, "click");
974                 }
975                 else if (target.hasAttribute("aria-expanded") &&
976                     (target.getAttribute("role") == "row" ||
977                     target.getAttribute("role") == "listitem"))
978                 {
979                     var toggleElem = Css.hasClass(target, "spyHeadTable") ?
980                         target.getElementsByClassName("spyTitleCol").item(0) : target;
981                     if (toggleElem)
982                         this.dispatchMouseEvent(toggleElem, "click");
983                 }
984                 break;
985 
986             case KeyEvent.DOM_VK_SPACE:
987                 if (this.isFocusObject(target) && target.hasAttribute("role", "checkbox"))
988                 {
989                     this.dispatchMouseEvent(target, "click");
990                     var objectBox = Dom.getAncestorByClass(target, "hasBreakSwitch");
991                     if (objectBox)
992                     {
993                         target.setAttribute("aria-checked",
994                             Css.hasClass(objectBox, "breakForError") + "");
995                     }
996                 }
997                 else if (Css.hasClass(target, "breakpointRow"))
998                 {
999                     var checkbox = target.getElementsByClassName("breakpointCheckbox").item(0);
1000                     if (checkbox)
1001                     {
1002                         target.setAttribute("aria-checked", checkbox.checked ? "false" : "true");
1003                         this.dispatchMouseEvent(checkbox, "click");
1004                     }
1005                 }
1006                 break;
1007 
1008             case KeyEvent.DOM_VK_DELETE:
1009                 if (Css.hasClass(target, "breakpointRow"))
1010                 {
1011                     var closeBtn = target.getElementsByClassName("closeButton").item(0);
1012                     if (closeBtn)
1013                     {
1014                         var prevBreakpoint = Dom.getPreviousByClass(target, "breakpointRow");
1015                         if (prevBreakpoint)
1016                             this.makeFocusable(prevBreakpoint, true);
1017                         Firebug.chrome.window.document.commandDispatcher.rewindFocus();
1018                         this.dispatchMouseEvent(closeBtn, "click");
1019                     }
1020                 }
1021                 break;
1022         }
1023     },
1024 
1025     focusPanelRow: function(panel, row)
1026     {
1027         var panelA11y = this.getPanelA11y(panel);
1028         if (!panelA11y || !row)
1029             return;
1030         this.modifyPanelRow(panel, row, false);
1031 
1032         //allows up / down navigation in columns, if columns are used in this panel
1033         if (panelA11y.cellIndex !== undefined && row.cells && row.cells[panelA11y.cellIndex])
1034         {
1035             var cell = row.cells[panelA11y.cellIndex];
1036             if (!Css.hasClass(cell, "a11yFocus"))
1037                 cell = Dom.getChildByClass(cell, "a11yFocus");
1038             this.focus(cell);
1039         }
1040         // for Net Panel. Focus selected tab rather than the tab list
1041         else if (Css.hasClass(row, "netInfoTabs"))
1042         {
1043             var tabs = row.getElementsByClassName("netInfoTab");
1044             tabs = Array.filter(tabs, function(e, i, a)
1045             {
1046                 return e.hasAttribute("selected");
1047             });
1048             this.focus(tabs.length > 0 ? tabs[0] : row);
1049         }
1050         else
1051         {
1052             this.focus(row);
1053         }
1054     },
1055 
1056     getRowIndex: function(rows, target)
1057     {
1058         return Array.indexOf(rows, target);
1059     },
1060 
1061     getAncestorRow: function(elem, useSubRow)
1062     {
1063         return Dom.getAncestorByClass(elem, useSubRow ? "focusRow" : "outerFocusRow");
1064     },
1065 
1066     onConsoleMouseDown: function(event)
1067     {
1068         var node = Dom.getAncestorByClass(event.target, "focusRow");
1069         if (node)
1070         {
1071             this.modifyPanelRow(Firebug.getElementPanel(node), node, false);
1072         }
1073         else
1074         {
1075             node = Dom.getAncestorByClass(event.target, "memberRow");
1076             if (!node)
1077                 return;
1078             var focusRow = node.getElementsByClassName("focusRow").item(0);
1079             if (!focusRow)
1080                 return;
1081 
1082             this.focusPanelRow(Firebug.getElementPanel(focusRow), focusRow);
1083             node = Dom.getAncestorByClass(event.target, "memberLabel")
1084             if (!(node && Css.hasClass(node, "hasChildren")))
1085                 Events.cancelEvent(event);
1086         }
1087     },
1088 
1089     getValidRow: function(rows, index)
1090     {
1091         var min = 0; var max = rows.length -1;
1092         if (index < min || index > max)
1093             index = index < min ? 0 : max;
1094         return rows[index];
1095     },
1096 
1097     getFocusObjects: function(container)
1098     {
1099         var nodes = container.getElementsByClassName("a11yFocus")
1100         return Array.filter(nodes, this.isVisibleByStyle, this);
1101     },
1102 
1103     modifyConsoleRow: function(panel, row, inTabOrder)
1104     {
1105         if (this.isDirCell(row))
1106         {
1107             this.modifyMemberRow(panel, row, inTabOrder);
1108         }
1109         else if (this.isProfileRow(row))
1110         {
1111             this.modifyProfileRow(panel, row, inTabOrder);
1112         }
1113         else if (this.isOuterFocusRow(row, true))
1114         {
1115             if (Css.hasClass(row, "spyHeadTable") || Css.hasClass(row, "netInfoTabs"))
1116                 this.modifyNetRow(panel, row, row.getAttribute("tabindex") === "0");
1117             else
1118                 this.modifyLogRow(panel, row, row.getAttribute("tabindex") === "0");
1119         }
1120         else return;
1121     },
1122 
1123     modifyProfileRow: function(panel, row, inTabOrder)
1124     {
1125         var panelA11y = this.getPanelA11y(panel);
1126         if (!panelA11y || !row)
1127             return;
1128 
1129         this.makeFocusable(row, inTabOrder);
1130         var focusObjects = this.getFocusObjects(row);
1131         Array.forEach(focusObjects, function(e, i, a)
1132         {
1133             this.makeFocusable(e);
1134             if (Css.hasClass(e.parentNode, "profileCell"))
1135                 e.setAttribute("role", "gridcell");
1136         }, this);
1137     },
1138 
1139     onConsoleSearchMatchFound: function(panel, text, matches)
1140     {
1141         var panelA11y = this.getPanelA11y(panel);
1142         if (!panelA11y)
1143             return;
1144 
1145         var matchFeedback = "";
1146         if (!matches || matches.length == 0)
1147         {
1148             matchFeedback = Locale.$STRF("a11y.updates.no matches found", [text]);
1149         }
1150         else
1151         {
1152             matchFeedback = Locale.$STRF("a11y.updates.match found in logrows",
1153                 [text, matches.length]);
1154         }
1155         this.updateLiveElem(panel, matchFeedback, true); //should not use alert
1156     },
1157 
1158     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1159     // HTML Panel
1160 
1161     onHTMLKeyPress: function(event)
1162     {
1163         var target = event.target;
1164         var keyCode = event.keyCode || (event.type == "keypress" ? event.charCode : null);
1165         if ([KeyEvent.DOM_VK_RETURN, KeyEvent.DOM_VK_SPACE,
1166              KeyEvent.DOM_VK_F2].indexOf(keyCode) == -1)
1167         {
1168             return;
1169         }
1170         if (!Css.hasClass(target, "nodeLabelBox"))
1171             return;
1172 
1173         var panel = Firebug.getElementPanel(target);
1174         switch (keyCode)
1175         {
1176             case KeyEvent.DOM_VK_RETURN:
1177             case KeyEvent.DOM_VK_SPACE:
1178                 var isEnter = (keyCode == KeyEvent.DOM_VK_RETURN);
1179                 var nodeLabels = null;
1180                 if (isEnter)
1181                 {
1182                     var nodeLabels = target.getElementsByClassName("nodeName");
1183                     if (nodeLabels.length > 0)
1184                     {
1185                         Firebug.Editor.startEditing(nodeLabels[0]);
1186                         Events.cancelEvent(event);
1187                     }
1188                 }
1189                 if (!isEnter || nodeLabels.length == 0)
1190                 {
1191                     var nodeBox = Dom.getAncestorByClass(target, "nodeBox");
1192                     if (nodeBox.repObject && panel.editNewAttribute)
1193                     {
1194                         panel.editNewAttribute(nodeBox.repObject)
1195                         Events.cancelEvent(event);
1196                     }
1197                 }
1198                 break;
1199 
1200             case KeyEvent.DOM_VK_F2:
1201                 if (Css.hasClass(target.parentNode.parentNode, "textNodeBox"))
1202                 {
1203                     var textNode = Dom.getChildByClass(target, "nodeText");
1204                     if (textNode)
1205                         Firebug.Editor.startEditing(textNode);
1206                 }
1207                 break;
1208         }
1209     },
1210 
1211     onHTMLFocus: function(event)
1212     {
1213         if (Css.hasClass(event.target, "nodeLabelBox"))
1214         {
1215             this.dispatchMouseEvent(event.target, "mouseover");
1216             var nodeLabel = Dom.getAncestorByClass(event.target, "nodeLabel");
1217             if (nodeLabel)
1218                 Css.setClass(nodeLabel, "focused");
1219             event.target.setAttribute("aria-selected", "true");
1220             Events.cancelEvent(event);
1221         }
1222     },
1223 
1224     onHTMLBlur: function(event)
1225     {
1226         if (Css.hasClass(event.target, "nodeLabelBox"))
1227         {
1228             this.dispatchMouseEvent(event.target, "mouseout");
1229             var nodeLabel = Dom.getAncestorByClass(event.target, "nodeLabel");
1230             if (nodeLabel)
1231                 Css.removeClass(nodeLabel, "focused");
1232             event.target.setAttribute("aria-selected", "false");
1233             Events.cancelEvent(event);
1234         }
1235     },
1236 
1237     onObjectBoxSelected: function(objectBox, forceFocus)
1238     {
1239         var panel = Firebug.getElementPanel(objectBox);
1240 
1241         // See issue 5934
1242         if (panel.editing)
1243             return;
1244 
1245         var panelA11y = this.getPanelA11y(panel);
1246         if (!panelA11y)
1247             return;
1248         var label = objectBox.firstChild.getElementsByClassName("nodeLabelBox").item(0);
1249         if (label)
1250         {
1251             this.makeFocusable(label, true);
1252             if (this.panelHasFocus(panel) || forceFocus)
1253                 this.focus(label);
1254         }
1255     },
1256 
1257     onObjectBoxUnselected: function(objectBox)
1258     {
1259         if (!this.isEnabled() || !objectBox)
1260             return;
1261         var label = objectBox.firstChild.getElementsByClassName("nodeLabelBox").item(0);
1262         if (label)
1263             this.makeUnfocusable(label, true);
1264     },
1265 
1266     onHTMLSearchMatchFound: function(panel, match)
1267     {
1268         var panelA11y = this.getPanelA11y(panel);
1269         if (!panelA11y)
1270             return;
1271 
1272         var node = match.node;
1273         var elem;
1274         var matchFeedback = "";
1275         switch (node.nodeType)
1276         {
1277             case Node.ELEMENT_NODE:
1278                 elem = node;
1279                 matchFeedback += Locale.$STRF("a11y.updates.match found in element",
1280                     [match.match[0], elem.nodeName, Xpath.getElementTreeXPath(elem)]);
1281                 break;
1282 
1283             case Node.ATTRIBUTE_NODE:
1284                 elem = node.ownerElement;
1285                 matchFeedback += Locale.$STRF("a11y.updates.match found in attribute",
1286                     [match.match[0], node.name, node.value, elem.nodeName,
1287                         Xpath.getElementTreeXPath(elem)]);
1288                 break;
1289 
1290             case Node.TEXT_NODE:
1291                 elem = node.parentNode;
1292                 matchFeedback += Locale.$STRF("a11y.updates.match found in text content",
1293                     [match.match[0], match.match.input]);
1294                 break;
1295         }
1296         this.updateLiveElem(panel, matchFeedback, true); //should not use alert
1297     },
1298 
1299     onHTMLSearchNoMatchFound: function(panel, text)
1300     {
1301         this.updateLiveElem(panel, Locale.$STRF("a11y.updates.no matches found", [text]), true);
1302     },
1303 
1304     moveToSearchMatch: function()
1305     {
1306         if (!this.isEnabled())
1307             return;
1308         var panel = Firebug.chrome.getSelectedPanel();
1309         var panelA11y = this.getPanelA11y(panel);
1310         if (!panelA11y || !panel.searchable)
1311             return;
1312 
1313         var popup = Firebug.chrome.$("fbSearchOptionsPopup");
1314         if (popup)
1315             popup.hidePopup();
1316         var type = panel.searchType ? panel.searchType : panelA11y.type;
1317         switch (type)
1318         {
1319             case "html":
1320                 var match = panel.lastSearch.lastMatch;
1321                 if (!match)
1322                     return;
1323                 var nodeBox = panel.lastSearch.openToNode(match.node, match.isValue);
1324                 if (!nodeBox)
1325                     return;
1326 
1327                 nodeBox = Dom.getAncestorByClass(nodeBox, "nodeBox");
1328                 //select call will not trigger focus because focus is outside the HTML panel (i.e. the search field),
1329                 panel.select(nodeBox.repObject, true);
1330                 // Manually force selected node to be focused
1331                 this.onObjectBoxSelected(nodeBox, true);
1332                 break;
1333 
1334             case "css":
1335                 if (panel.currentSearch && panel.currentSearch.currentNode)
1336                 {
1337                     var focusRow = Dom.getAncestorByClass(panel.currentSearch.currentNode,
1338                         "focusRow");
1339                     if (focusRow)
1340                         this.focusPanelRow(panel, focusRow);
1341                 }
1342                 break
1343 
1344             case "script":
1345                 if (panel.currentSearch && panel.selectedSourceBox)
1346                 {
1347                     var box = panel.selectedSourceBox;
1348                     var lineNo = panel.currentSearch.mark;
1349                     box.a11yCaretLine = lineNo + 1;
1350                     box.a11yCaretOffset = 0;
1351                     panel.scrollToLine(box.repObject.href, lineNo,
1352                         panel.jumpHighlightFactory(lineNo+1, panel.context));
1353                     var viewport = box.getElementsByClassName("sourceViewport").item(0);
1354                     if (viewport)
1355                     {
1356                         this.focus(viewport);
1357                         this.insertCaretIntoLine(panel, box);
1358                     }
1359                 }
1360                 break;
1361 
1362             case "dom":
1363                 if (panel.currentSearch && panel.currentSearch.currentNode)
1364                 {
1365                     var focusRow =
1366                       panel.currentSearch.currentNode.getElementsByClassName("focusRow").item(0);
1367                     if (focusRow)
1368                         this.focusPanelRow(panel, focusRow);
1369                 }
1370                 break;
1371 
1372             case "net":
1373                 if (panel.currentSearch && panel.currentSearch.currentNode)
1374                 {
1375                     var focusRow = Dom.getAncestorByClass(panel.currentSearch.currentNode,
1376                         "focusRow");
1377                     if (focusRow)
1378                         this.focusPanelRow(panel, focusRow);
1379                 }
1380                 break;
1381         }
1382     },
1383 
1384     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1385     // CSS Panel
1386 
1387     onCSSKeyPress: function(event)
1388     {
1389         var target = event.target;
1390         var keyCode = event.keyCode || (event.type == "keypress" ? event.charCode : null);
1391         if (!this.isFocusRow(target) || event.altKey)
1392             return;
1393 
1394         if ([KeyEvent.DOM_VK_RETURN, KeyEvent.DOM_VK_SPACE, KeyEvent.DOM_VK_PAGE_UP,
1395             KeyEvent.DOM_VK_PAGE_DOWN, KeyEvent.DOM_VK_END, KeyEvent.DOM_VK_HOME,
1396             KeyEvent.DOM_VK_UP, KeyEvent.DOM_VK_DOWN].indexOf(keyCode) == -1)
1397         {
1398             return; //not interested in any other keys than arrows, pg, home/end, space & enter
1399         }
1400         var panel = Firebug.getElementPanel(target)
1401         var panelA11y = this.getPanelA11y(panel);
1402         if (!panelA11y)
1403             return;
1404 
1405         switch (keyCode)
1406         {
1407             case KeyEvent.DOM_VK_UP:
1408             case KeyEvent.DOM_VK_DOWN:
1409                 var goUp = keyCode == 38;
1410                 if (Events.isControl(event))
1411                 {
1412                     if (event.shiftKey)
1413                     {
1414                         var node = this[goUp ? "getPreviousByClass" : "getNextByClass"](target,
1415                             "cssInheritHeader", panel.panelNode);
1416                         if (node)
1417                             this.focusPanelRow(panel, node);
1418                         else if (goUp)
1419                            this.focusEdgeCSSRow(panel, target, true);
1420                     }
1421                     else
1422                         this.focusSiblingHeadRow(panel, target, goUp);
1423                 }
1424                 else
1425                     this.focusSiblingCSSRow(panel, target, goUp);
1426                 break;
1427 
1428             case KeyEvent.DOM_VK_END:
1429             case KeyEvent.DOM_VK_HOME:
1430                 if (Events.isControl(event))
1431                     this.focusEdgeHeadRow(panel, target, keyCode == 36);
1432                 else
1433                     this.focusEdgeCSSRow(panel, target, keyCode == 36);
1434                 break;
1435 
1436             case KeyEvent.DOM_VK_PAGE_UP:
1437             case KeyEvent.DOM_VK_PAGE_DOWN:
1438                 if (Events.isControl(event))
1439                     this.focusPageSiblingHeadRow(panel, target, keyCode == 33);
1440                 else
1441                     this.focusPageSiblingCSSRow(panel, target, keyCode == 33);
1442                 break;
1443 
1444             case KeyEvent.DOM_VK_RETURN:
1445                 if (Css.hasClass(target, "cssProp"))
1446                 {
1447                     var node = Dom.getChildByClass(target, "cssPropName");
1448                     if (node)
1449                         Firebug.Editor.startEditing(node);
1450                     Events.cancelEvent(event);
1451                 }
1452                 else if (Css.hasClass(target, "cssHead"))
1453                 {
1454                     var node = Dom.getChildByClass(target, "cssSelector");
1455                     if (node && Css.hasClass(node, "editable"))
1456                         Firebug.Editor.startEditing(node);
1457                     Events.cancelEvent(event);
1458                 }
1459                 else if (Css.hasClass(target, "importRule"))
1460                 {
1461                     var node = Dom.getChildByClass(target, "objectLink");
1462                     if (node)
1463                         this.dispatchMouseEvent(node, "click");
1464                 }
1465                 break;
1466 
1467             case KeyEvent.DOM_VK_SPACE:
1468                 if (Css.hasClass(target, "cssProp"))
1469                 {
1470                     //our focus is about to be wiped out, we'll try to get it back after
1471                     panelA11y.reFocusId = Xpath.getElementXPath(target);
1472                     panel.disablePropertyRow(target);
1473                     if (panel.name == "stylesheet")
1474                     {
1475                         target.setAttribute("aria-checked",
1476                             !Css.hasClass(target, "disabledStyle"));
1477                     }
1478                     Events.cancelEvent(event);
1479                 }
1480                 break;
1481         }
1482         if (!event.shiftKey)
1483             event.preventDefault();
1484     },
1485 
1486     onCSSMouseDown: function(event)
1487     {
1488         var row = Dom.getAncestorByClass(event.target, "focusRow");
1489         if (row)
1490             this.modifyPanelRow(Firebug.getElementPanel(row), row, false);
1491     },
1492 
1493     focusSiblingCSSRow: function(panel, target, goUp)
1494     {
1495         var newRow = this[goUp ? "getPreviousByClass" : "getNextByClass"](target, "focusRow",
1496             panel.panelNode);
1497         if (!newRow)
1498             return;
1499         this.focusPanelRow(panel, newRow, false);
1500     },
1501 
1502     focusPageSiblingCSSRow: function(panel, target, goUp)
1503     {
1504         var rows = this.getFocusRows(panel);
1505         var index = this.getRowIndex(rows, target);
1506         var newRow = this.getValidRow(rows, goUp ? index - 10 : index + 10);
1507         this.focusPanelRow(panel, newRow, false);
1508     },
1509 
1510     focusEdgeCSSRow: function(panel, target, goUp)
1511     {
1512         var rows = this.getFocusRows(panel);
1513         var newRow = this.getValidRow(rows, goUp ? 0 : rows.length -1);
1514         this.focusPanelRow(panel, newRow, false);
1515     },
1516 
1517     getHeadRowsAndIndex: function(panel, elem)
1518     {
1519         var rows = this.getFocusRows(panel);
1520         var headRow = Css.hasClass(elem, "cssHead") ?
1521             elem : Dom.getPreviousByClass(elem, "cssHead");
1522         var headRows = Array.filter(rows, function(e, i, a) {
1523             return Css.hasClass(e, "cssHead");
1524         });
1525         var index = Array.indexOf(headRows, headRow);
1526         if (index == -1)
1527             index = 0;
1528         return [headRows, index];
1529     },
1530 
1531     focusSiblingHeadRow: function(panel, elem, goUp)
1532     {
1533         var rowInfo = this.getHeadRowsAndIndex(panel, elem);
1534         var newRow = this.getValidRow(rowInfo[0], goUp ? rowInfo[1] - 1 : rowInfo[1] + 1);
1535         this.focusPanelRow(panel, newRow, false);
1536     },
1537 
1538     focusPageSiblingHeadRow: function(panel, elem, goUp)
1539     {
1540         var rowInfo = this.getHeadRowsAndIndex(panel, elem);
1541         var newRow = this.getValidRow(rowInfo[0], goUp ? rowInfo[1] - 10 : rowInfo[1] + 10);
1542         this.focusPanelRow(panel, newRow, false);
1543     },
1544 
1545     focusEdgeHeadRow: function(panel, elem, goUp)
1546     {
1547         var rowInfo = this.getHeadRowsAndIndex(panel, elem);
1548         var newRow = this.getValidRow(rowInfo[0], goUp ? 0 : rowInfo[0].length - 1);
1549         this.focusPanelRow(panel, newRow, false);
1550     },
1551 
1552     onBeforeCSSRulesAdded: function(panel)
1553     {
1554         // Panel content is about to be recreated, possibly wiping out focus.
1555         // Use the focused element's xpath to remember which rule had focus,
1556         // so that it can be refocused when the panel content is drawn again
1557         var panelA11y = this.getPanelA11y(panel);
1558         if (!panelA11y || !this.panelHasFocus(panel))
1559             return;
1560         if (panelA11y.tabStop && Css.hasClass(panelA11y.tabStop, "focusRow"))
1561             panelA11y.reFocusId = Xpath.getElementXPath(panelA11y.tabStop);
1562     },
1563 
1564     onCSSRulesAdded: function(panel, rootNode)
1565     {
1566         var panelA11y = this.getPanelA11y(panel);
1567         if (!panelA11y)
1568             return;
1569         var row;
1570         if (panelA11y.reFocusId)
1571         {   //we need to put focus back to where it was before it was wiped out
1572             var reFocusRows = Xpath.getElementsByXPath(rootNode.ownerDocument,
1573                 panelA11y.reFocusId);
1574             panelA11y.reFocusId = null;
1575             if (reFocusRows.length > 0)
1576             {
1577                 row = reFocusRows[0];
1578                 this.modifyPanelRow(panel, row, true);
1579                 this.focus(row, true);
1580                 this.setPanelTabStop(panel, row);
1581                 return;
1582             }
1583         }
1584         //no refocus needed, just make first rule the panel's tab stop
1585         row = rootNode.getElementsByClassName("focusRow").item(0);
1586         this.modifyPanelRow(panel, row, true);
1587         return;
1588     },
1589     //applies a11y changes (keyboard and screen reader related) to an individual row
1590     //To improve performance, this only happens when absolutely necessary, e.g. when the user navigates to the row in question
1591 
1592     modifyCSSRow: function(panel, row, inTabOrder)
1593     {
1594         if (!panel || !row)
1595             return;
1596         var rule = Dom.getAncestorByClass(row, "cssRule");
1597         if (inTabOrder)
1598             this.setPanelTabStop(panel, row);
1599         else
1600             this.makeFocusable(row);
1601         if (rule && !Css.hasClass(rule, "a11yModified"))
1602         {
1603             var listBox = rule.getElementsByClassName("cssPropertyListBox").item(0);
1604             var selector = rule.getElementsByClassName("cssSelector").item(0);
1605             if (listBox && selector)
1606             {
1607                 listBox.setAttribute("aria-label",
1608                     Locale.$STRF("a11y.labels.declarations for selector", [selector.textContent]));
1609             }
1610             Css.setClass(rule, "a11yModified")
1611         }
1612         if (Css.hasClass(row, "cssHead"))
1613         {
1614             if (panel.name == "css")
1615             {
1616                 var sourceLink = rule.parentNode.lastChild;
1617                 if (sourceLink && Css.hasClass(sourceLink, "objectLink"))
1618                 {
1619                     row.setAttribute("aria-label", row.textContent + " " +
1620                         Locale.$STRF("a11y.labels.defined in file", [sourceLink.textContent]));
1621                 }
1622             }
1623         }
1624         else if (Css.hasClass(row, "cssProp"))
1625         {
1626             row.setAttribute("aria-checked", !Css.hasClass(row, "disabledStyle"));
1627             if (Css.hasClass(row, "cssOverridden"))
1628             {
1629                 row.setAttribute("aria-label", Locale.$STR("aria.labels.overridden") + " " +
1630                     row.textContent);
1631             }
1632         }
1633         return;
1634     },
1635 
1636     onCSSPanelContextMenu: function(event)
1637     {
1638         var panelA11y = this.getPanelA11y(panel);
1639         if (!panelA11y)
1640             return;
1641 
1642         if (event.button == 0) //the event was created by keyboard, not right mouse click
1643         {
1644             var panel = Firebug.getElementPanel(event.target);
1645             if (panel && Css.hasClass(event.target, "focusRow"))
1646             {
1647                 var node = event.target;
1648                 if (panel.name == "css")
1649                 {
1650                     if (Css.hasClass(event.target, "cssHead"))
1651                     {
1652                         node = event.target.parentNode.getElementsByClassName("objectLink")
1653                             .item(0);
1654                     }
1655                     else if (Css.hasClass(event.target, "cssInheritHeader"))
1656                     {
1657                         node = event.target.getElementsByClassName("objectLink").item(0);
1658                     }
1659 
1660                     if (!node || Css.hasClass(node, "collapsed"))
1661                     {
1662                         node = event.target;
1663                     }
1664                 }
1665                 //these context menu options are likely to destroy current focus
1666                 panelA11y.reFocusId = Xpath.getElementXPath(event.target);
1667                 document.popupNode = node;
1668                 Firebug.chrome.$("fbContextMenu").openPopup(node, "overlap", 0,0,true);
1669                 Events.cancelEvent(event); //no need for default handlers anymore
1670             }
1671         }
1672     },
1673 
1674     onCSSSearchMatchFound: function(panel, text, matchRow)
1675     {
1676         var panelA11y = this.getPanelA11y(panel);
1677         if (!panelA11y || !text)
1678             return;
1679 
1680         if (!matchRow)
1681         {
1682             this.updateLiveElem(panel,
1683                 Locale.$STRF("a11y.updates.no matches found", [text]), true); //should not use alert
1684             return;
1685         }
1686         var matchFeedback = "";
1687         var matchType = "";
1688         var selector;
1689         if (Css.hasClass(matchRow, "cssSelector"))
1690         {
1691             matchFeedback = " " + Locale.$STRF("a11y.updates.match found in selector",
1692                 [text, matchRow.textContent]);
1693         }
1694         else
1695         {
1696             selector = Dom.getPreviousByClass(matchRow, "cssSelector");
1697             selector = selector ? selector.textContent : "";
1698             if (Css.hasClass(matchRow, "cssPropName") || Css.hasClass(matchRow, "cssPropValue"))
1699             {
1700                 var propRow = Dom.getAncestorByClass(matchRow, "cssProp");
1701                 if (propRow)
1702                 {
1703                     matchFeedback = Locale.$STRF("a11y.updates.match found in style declaration",
1704                         [text, propRow.textContent, selector]);
1705                 }
1706             }
1707         }
1708         this.updateLiveElem(panel, matchFeedback, true); // should not use alert
1709     },
1710 
1711     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1712     // Layout Panel
1713 
1714     onLayoutBoxCreated: function(panel, node, detailsObj)
1715     {
1716         var panelA11y = this.getPanelA11y(panel);
1717         if (!panelA11y)
1718             return;
1719 
1720         var focusGroups = node.getElementsByClassName("focusGroup");
1721         Array.forEach(focusGroups, function(e, i, a) {
1722             this.makeFocusable(e, Css.hasClass(e, "positionLayoutBox"));
1723             e.setAttribute("role", "group");
1724             e.setAttribute("aria-label", this.getLayoutBoxLabel(e, detailsObj));
1725             e.setAttribute("aria-setsize", a.length);
1726             e.setAttribute("aria-posinset", i + 1);
1727         }, this);
1728     },
1729 
1730     getLayoutBoxLabel: function(elem, detailsObj)
1731     {
1732         var className = elem.className.match(/\b(\w+)LayoutBox\b/);
1733         if (!className)
1734             return "";
1735 
1736         var styleName = className[1];
1737         var output = "";
1738         switch (styleName)
1739         {
1740             case "position":
1741                 output += Css.hasClass(elem, "blankEdge") ?
1742                     "" : Locale.$STR("a11y.layout.position");
1743                 styleName = "outer";
1744                 break;
1745 
1746             case "margin":
1747                 output += Locale.$STR("a11y.layout.margin");
1748                 break;
1749 
1750             case "border":
1751                 output += Locale.$STR("a11y.layout.border");
1752                 break;
1753 
1754             case "padding":
1755                 output += Locale.$STR("a11y.layout.padding");
1756                 break;
1757 
1758             case "content":
1759                 output += Locale.$STR("a11y.layout.size");
1760                 break;
1761         }
1762         output += ": ";
1763         var valNames = [];
1764         var vals = {};
1765         switch (styleName)
1766         {
1767             case "outer":
1768                 valNames = ["top", "left", "position", "z-index"];
1769                 vals.top = detailsObj[styleName + "Top"];
1770                 vals.left = detailsObj[styleName + "Left"];
1771                 vals.position = detailsObj.position;
1772                 vals["z-index"] = detailsObj.zIndex;
1773                 break;
1774 
1775             case "content":
1776                 valNames = ["width", "height"]
1777                 vals.width = detailsObj["width"];
1778                 vals.height = detailsObj["height"];
1779                 break;
1780 
1781             default:
1782                 valNames = ["top", "right", "bottom", "left"];
1783                 vals.top = detailsObj[styleName + "Top"];
1784                 vals.right = detailsObj[styleName + "Right"];
1785                 vals.bottom = detailsObj[styleName + "Bottom"];
1786                 vals.left = detailsObj[styleName + "Left"];
1787                 break;
1788         }
1789 
1790         for (var i = 0; i < valNames.length; i++)
1791         {
1792             output += Locale.$STR("a11y.layout." + valNames[i]) + " = " + vals[valNames[i]];
1793             output += i == valNames.length -1 ? "" : ", ";
1794         }
1795         return output;
1796     },
1797 
1798     onLayoutKeyPress: function(event)
1799     {
1800         var target = event.target;
1801         var keyCode = event.keyCode || (event.type == "keypress" ? event.charCode : null);
1802         if ([KeyEvent.DOM_VK_RETURN, KeyEvent.DOM_VK_LEFT, KeyEvent.DOM_VK_UP,
1803             KeyEvent.DOM_VK_RIGHT, KeyEvent.DOM_VK_DOWN].indexOf(keyCode) == -1)
1804         {
1805             return;
1806         }
1807         if (!Css.hasClass(target, "focusGroup"))
1808             return;
1809 
1810         var panel = Firebug.getElementPanel(target);
1811         switch (keyCode)
1812         {
1813             case KeyEvent.DOM_VK_LEFT:
1814             case KeyEvent.DOM_VK_UP:
1815             case KeyEvent.DOM_VK_RIGHT:
1816             case KeyEvent.DOM_VK_DOWN:
1817                 var node, goLeft = (keyCode == KeyEvent.DOM_VK_LEFT ||
1818                     keyCode == KeyEvent.DOM_VK_UP);
1819                 if (goLeft)
1820                     node = Dom.getAncestorByClass(target.parentNode, "focusGroup");
1821                 else
1822                     node = Dom.getChildByClass(target, "focusGroup");
1823 
1824                 if (node)
1825                     this.focus(node);
1826                 break;
1827 
1828             case KeyEvent.DOM_VK_RETURN:
1829                 var editable = target.getElementsByClassName("editable").item(0);
1830                 if (editable)
1831                     Firebug.Editor.startEditing(editable);
1832                 Events.cancelEvent(event);
1833                 break;
1834         }
1835     },
1836 
1837     onLayoutFocus: function(event)
1838     {
1839         if (Css.hasClass(event.target, "focusGroup"))
1840         {
1841             this.dispatchMouseEvent(event.target, "mouseover");
1842             this.setPanelTabStop(Firebug.getElementPanel(event.target), event.target);
1843         }
1844     },
1845 
1846     onLayoutBlur: function(event)
1847     {
1848         if (Css.hasClass(event.target, "focusGroup"))
1849             this.dispatchMouseEvent(event.target, "mouseout");
1850     },
1851 
1852     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1853     // Inline Editing
1854     onInlineEditorShow: function(panel, editor)
1855     {
1856         var panelA11y = this.getPanelA11y(panel);
1857         if (!panelA11y)
1858             return;
1859 
1860         //recreate the input element rather than reusing the old one, otherwise AT won't pick it up
1861         editor.input.onkeypress = editor.input.oninput = editor.input.onoverflow = null;
1862         editor.inputTag.replace({}, editor.box.childNodes[1].firstChild, editor);
1863         editor.input = editor.box.childNodes[1].firstChild.firstChild;
1864     },
1865 
1866     onBeginEditing: function(panel, editor, target, value)
1867     {
1868         var panelA11y = this.getPanelA11y(panel);
1869         if (!panelA11y)
1870             return;
1871 
1872         switch (panelA11y.type)
1873         {
1874             case "html":
1875                 var tagName= nodeName = null;
1876                 var setSize = posInSet = 0; var setElems;
1877                 var label = Locale.$STR("a11y.labels.inline editor") + ": ";
1878                 if (Css.hasClass(target, "nodeName") || Css.hasClass(target, "nodeValue"))
1879                 {
1880                     var isName = Css.hasClass(target, "nodeName");
1881                     setElems = target.parentNode.parentNode
1882                         .getElementsByClassName(isName ? "nodeName" : "nodeValue");
1883                     setSize = (setElems.length * 2);
1884                     posInSet = ((Array.indexOf(setElems, target) + 1) * 2) - (isName ? 1 : 0);
1885                     editor.input.setAttribute("role", "listitem");
1886                     editor.input.setAttribute("aria-setsize", setSize);
1887                     editor.input.setAttribute("aria-posinset", posInSet);
1888                     nodeTag = Dom.getPreviousByClass(target, "nodeTag");
1889                     if (!isName)
1890                     {
1891                         nodeName = Dom.getPreviousByClass(target, "nodeName");
1892                         label += Locale.$STRF("a11y.labels.value for attribute in element",
1893                             [nodeName.textContent, nodeTag.textContent]);
1894                     }
1895                     else
1896                     {
1897                         label += Locale.$STRF("a11y.label.attribute for element",
1898                             [nodeTag.textContent]);
1899                     }
1900                 }
1901                 else if (Css.hasClass(target, "nodeText"))
1902                 {
1903                     nodeTag = Dom.getPreviousByClass(target, "nodeTag");
1904                     label += Locale.$STRF("a11y.labels.text contents for element",
1905                         [nodeTag.textContent]);
1906                 }
1907                 editor.input.setAttribute("aria-label", label);
1908                 break;
1909 
1910             case "css":
1911             case "stylesheet":
1912                 var selector = Dom.getPreviousByClass(target, "cssSelector");
1913                 selector = selector ? selector.textContent : "";
1914                 var label = Locale.$STR("a11y.labels.inline editor") + ": ";
1915                 if (Css.hasClass(target, "cssPropName"))
1916                 {
1917                     label += Locale.$STRF("a11y.labels.property for selector", [selector]);
1918                 }
1919                 else if (Css.hasClass(target, "cssPropValue"))
1920                 {
1921                     var propName = Dom.getPreviousByClass(target, "cssPropName");
1922                     propName = propName ? propName.textContent : "";
1923                     label += Locale.$STRF("a11y.labels.value property in selector",
1924                         [propName, selector]);
1925                 }
1926                 else if (Css.hasClass(target, "cssSelector"))
1927                 {
1928                     label += Locale.$STR("a11y.labels.css selector");
1929                 }
1930 
1931                 editor.input.setAttribute("aria-label", label);
1932                 editor.setAttribute("aria-autocomplete", "inline");
1933                 break;
1934 
1935             case "layout":
1936                 editor.input.setAttribute("aria-label", target.getAttribute("aria-label"));
1937                 break;
1938 
1939             case "dom":
1940             case "domSide":
1941                 if (target.cells && target.cells[1])
1942                     editor.input.setAttribute("aria-label", target.cells[1].textContent);
1943                 break;
1944         }
1945     },
1946 
1947     onInlineEditorClose: function(panel, target, removeGroup)
1948     {
1949         var panelA11y = this.getPanelA11y(panel);
1950         if (!panelA11y)
1951             return;
1952 
1953         switch (panelA11y.type)
1954         {
1955             case "layout":
1956                 var box = Dom.getAncestorByClass(target, "focusGroup")
1957                 if (box)
1958                     this.focus(box, true);
1959                 break;
1960 
1961             case "css":
1962             case "stylesheet":
1963                 var node = target.parentNode;
1964                 if (removeGroup)
1965                     node = this.getPreviousByClass(node, "focusRow", panel.panelNode);
1966                 if (node)
1967                     this.focusPanelRow(panel, node, true);
1968                 break;
1969 
1970             case "html":
1971                 var box = Dom.getAncestorByClass(target, "nodeBox")
1972                 if (box)
1973                     panel.select(box.repObject, true);
1974                 break;
1975 
1976             case "watches":
1977                 var node = target.getElementsByClassName("watchEditBox").item(0);
1978                 if (node)
1979                     this.focus(node, true);
1980                 break;
1981 
1982             case "script":
1983                 panel.selectedSourceBox.focus();
1984                 break;
1985         }
1986     },
1987 
1988     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1989     // Script Panel
1990 
1991     onStartDebugging: function(context)
1992     {
1993         if (!context)
1994             return;
1995 
1996         var panel = context.getPanel("script");
1997         var panelA11y = this.getPanelA11y(panel);
1998         if (!panelA11y)
1999             return;
2000 
2001         var frame = context.stoppedFrame;
2002         var fileName =  frame.script.fileName.split("/");  // XXXjjb I think this should be contxt.executingSourceFile.href
2003         fileName = fileName.pop();
2004         // XXXjjb the frame.functionName is often anonymous, since the compiler is lame.
2005         var alertString = Locale.$STRF("a11y.updates.script_suspended_on_line_in_file",
2006             [frame.line, frame.functionName, fileName]);
2007         this.updateLiveElem(panel, alertString, true);
2008         this.onShowSourceLink(panel, frame.line);
2009     },
2010 
2011     onShowSourceLink: function (panel, lineNo)
2012     {
2013         if (!this.isEnabled())
2014             return;
2015 
2016         var box = panel.selectedSourceBox;
2017         var viewport = box.getElementsByClassName("sourceViewport").item(0);
2018         box.a11yCaretLine = lineNo;
2019         if (viewport && this.panelHasFocus(panel))
2020         {
2021             this.focus(viewport);
2022             this.insertCaretIntoLine(panel, box, lineNo);
2023         }
2024     },
2025 
2026     onScriptKeyPress: function(event)
2027     {
2028         var target = event.target;
2029         var keyCode = event.keyCode || (event.type == "keypress" ? event.charCode : null);
2030         if (!Css.hasClass(target, "sourceViewport"))
2031             return;
2032 
2033         if ([KeyEvent.DOM_VK_RETURN, KeyEvent.DOM_VK_PAGE_UP, KeyEvent.DOM_VK_PAGE_DOWN,
2034             KeyEvent.DOM_VK_END, KeyEvent.DOM_VK_HOME, KeyEvent.DOM_VK_LEFT, KeyEvent.DOM_VK_UP,
2035             KeyEvent.DOM_VK_RIGHT, KeyEvent.DOM_VK_DOWN].indexOf(keyCode) == -1)
2036         {
2037             return;
2038         }
2039 
2040         var panel = Firebug.getElementPanel(target);
2041         var panelA11y = this.getPanelA11y(panel);
2042         if (!panelA11y)
2043             return;
2044 
2045         var box = panel.selectedSourceBox
2046         var lastLineNo = box.lastViewableLine;
2047         var firstLineNo = box.firstViewableLine;
2048         var caretDetails = this.getCaretDetails(event.target.ownerDocument);
2049         if (!caretDetails || caretDetails.length != 2)
2050             return;
2051 
2052         var lineNode = Dom.getAncestorByClass(caretDetails[0].parentNode, "sourceRow");
2053         if (!lineNode)
2054             return;
2055 
2056         var lineNo = parseInt(lineNode.getElementsByClassName("sourceLine").item(0).textContent);
2057         box.a11yCaretLine = lineNo;
2058         box.a11yCaretOffset = caretDetails[1];
2059         var newLineNo = 1;
2060         var linesToScroll = 0;
2061         var goUp;
2062         switch (keyCode)
2063         {
2064             case KeyEvent.DOM_VK_UP:
2065             case KeyEvent.DOM_VK_DOWN:
2066                 goUp = (keyCode == KeyEvent.DOM_VK_UP);
2067                 linesToScroll = goUp ? -1 : 1;
2068                 if (!Events.isControl(event))
2069                 {
2070                     if ((goUp && lineNo > firstLineNo + 1) ||
2071                         (!goUp && lineNo < lastLineNo - 1))
2072                     {
2073                         return;
2074                     }
2075 
2076                     box.a11yCaretLine = goUp ? lineNo - 1 : lineNo +1;
2077                 }
2078                 box.scrollTop = box.scrollTop + (linesToScroll * box.lineHeight);
2079                 break;
2080 
2081             case KeyEvent.DOM_VK_PAGE_UP:
2082             case KeyEvent.DOM_VK_PAGE_DOWN:
2083                 goUp = (keyCode == KeyEvent.DOM_VK_PAGE_UP);
2084                 if ((goUp && box.scrollTop == 0) ||
2085                     (!goUp && box.scrollTop == box.scrollHeight - box.clientHeight))
2086                 {
2087                     box.a11yCaretLine = goUp ? 0 : box.totalMax;
2088                     box.a11yCaretOffset = 0;
2089                     this.insertCaretIntoLine(panel, box);
2090                     Events.cancelEvent(event);
2091                     return;
2092                 }
2093                 box.a11yCaretLine = goUp ? lineNo - box.viewableLines : lineNo + box.viewableLines;
2094                 linesToScroll = goUp ? -box.viewableLines : box.viewableLines;
2095                 box.scrollTop = box.scrollTop + (linesToScroll * box.lineHeight);
2096                 Events.cancelEvent(event);
2097                 break;
2098 
2099             case KeyEvent.DOM_VK_HOME:
2100             case KeyEvent.DOM_VK_END:
2101                 goUp = (keyCode == KeyEvent.DOM_VK_HOME);
2102                 if (Events.isControl(event))
2103                 {
2104                     box.a11yCaretLine = goUp ? 0 : box.totalMax;
2105                     box.a11yCaretOffset = 0;
2106                     if ((goUp && box.scrollTop == 0) ||
2107                         (!goUp && box.scrollTop == box.scrollHeight - box.clientHeight))
2108                     {
2109                         this.insertCaretIntoLine(panel, box);
2110                     }
2111                     else
2112                     {
2113                         box.scrollTop = goUp ? 0 : box.scrollHeight - box.clientHeight;;
2114                     }
2115                     Events.cancelEvent(event);
2116                     return;
2117                 }
2118 
2119                 if (goUp)
2120                 {
2121                     //move caret to beginning of line. Override default behavior, as that would take the caret into the line number
2122                     this.insertCaretIntoLine(panel, box, lineNo, 0);
2123                     box.scrollLeft = 0; //in case beginning of line is scrolled out of view
2124                     Events.cancelEvent(event);
2125                 }
2126                 break;
2127 
2128             case KeyEvent.DOM_VK_RETURN:
2129                 var liveString = "";
2130                 var caretDetails = this.getCaretDetails(event.target.ownerDocument);
2131                 var lineNode = Dom.getAncestorByClass(caretDetails[0].parentNode, "sourceRow");
2132                 var lineNo = parseInt(lineNode.getElementsByClassName("sourceLine").item(0)
2133                     .textContent);
2134                 liveString += "Line " + lineNo;
2135                 if (lineNode.getAttribute("breakpoint") == "true")
2136                 {
2137                     var breakpointStr;
2138                     if (lineNode.getAttribute("disabledbreakpoint") == "true")
2139                         breakpointStr = "a11y.updates.has disabled breakpoint";
2140                     if (lineNode.getAttribute("condition") == "true")
2141                         breakpointStr = "a11y.updates.has conditional breakpoint";
2142                     liveString += ", " + Locale.$STR(breakpointStr);
2143                 }
2144                 if (lineNode.getAttribute("executable") == "true")
2145                     liveString += ", executable";
2146                 if (lineNode.getAttribute("exe_line") == "true")
2147                     liveString += ", currently stopped";
2148                 var sourceText = lineNode.getElementsByClassName("sourceRowText").item(0);
2149                 if (sourceText)
2150                     liveString += ": " + sourceText.textContent;
2151                 this.updateLiveElem(panel, liveString, true); //should not use alert
2152                 break;
2153         }
2154     },
2155 
2156     onScriptKeyUp: function(event)
2157     {
2158         var target = event.target;
2159         var keyCode = event.keyCode || (event.type == "keypress" ? event.charCode : null);
2160         if (!Css.hasClass(target, "sourceViewport"))
2161             return;
2162 
2163         if ([KeyEvent.DOM_VK_RETURN, KeyEvent.DOM_VK_PAGE_UP, KeyEvent.DOM_VK_PAGE_DOWN,
2164             KeyEvent.DOM_VK_END, KeyEvent.DOM_VK_HOME, KeyEvent.DOM_VK_LEFT, KeyEvent.DOM_VK_UP,
2165             KeyEvent.DOM_VK_RIGHT, KeyEvent.DOM_VK_DOWN].indexOf(keyCode) == -1)
2166         {
2167             return;
2168         }
2169 
2170         var panel = Firebug.getElementPanel(target);
2171         var panelA11y = this.getPanelA11y(panel);
2172         if (!panelA11y)
2173             return;
2174 
2175         var box = panel.selectedSourceBox
2176         var caretDetails = this.getCaretDetails(target.ownerDocument);
2177         var lineNode = Dom.getAncestorByClass(caretDetails[0].parentNode, "sourceRow");
2178         if (!lineNode)
2179             return;
2180 
2181         var lineNo = parseInt(lineNode.getElementsByClassName("sourceLine").item(0).textContent);
2182         box.a11yCaretLine = lineNo;
2183         box.a11yCaretOffset = caretDetails[1];
2184     },
2185 
2186     onScriptMouseUp: function(event)
2187     {
2188         var target = event.target;
2189         if (event.button !== 0)
2190             return;
2191 
2192         var panel = Firebug.getElementPanel(target);
2193         var panelA11y = this.getPanelA11y(panel);
2194         if (!panelA11y)
2195             return;
2196 
2197         var box = panel.selectedSourceBox
2198         var caretDetails = this.getCaretDetails(target.ownerDocument);
2199         var lineNode = null;
2200         if (caretDetails[0] && caretDetails[0].parentNode)
2201             lineNode = Dom.getAncestorByClass(caretDetails[0].parentNode, "sourceRow");
2202         if (!lineNode)
2203             return;
2204 
2205         var lineNo = parseInt(lineNode.getElementsByClassName("sourceLine").item(0).textContent);
2206         box.a11yCaretLine = lineNo;
2207         box.a11yCaretOffset = caretDetails[1];
2208     },
2209 
2210     onBeforeViewportChange: function(panel)
2211     {
2212         var panelA11y = this.getPanelA11y(panel);
2213         if (!panelA11y)
2214             return;
2215 
2216         var box = panel.selectedSourceBox;
2217         if (!box)
2218             return;
2219 
2220         this.insertCaretIntoLine(panel, box);
2221     },
2222 
2223     insertCaretIntoLine: function(panel, box, lineNo, offset)
2224     {
2225         var panelA11y = this.getPanelA11y(panel);
2226         if (!panelA11y || !box)
2227             return;
2228 
2229         if (typeof lineNo == "undefined")
2230             lineNo = box.a11yCaretLine ?  box.a11yCaretLine : 0;
2231         //to prevent the caret from (partially) being placed just out of sight,
2232         //adjust the viewable line boundaries by 1 (unless the current line is the first or last line)
2233         var lineAdjust = lineNo == 0 || lineNo == box.totalMax ? 0 : 1;
2234         var firstLine = box.firstViewableLine + lineAdjust;
2235         var lastLine = box.lastViewableLine - lineAdjust;
2236         if (lineNo < (firstLine) || lineNo > lastLine)
2237             box.a11yCaretLine = lineNo = lineNo < firstLine ? firstLine : lastLine;
2238         var node = box.getLineNode(lineNo);
2239         if (!node)
2240             return;
2241 
2242         if (typeof offset == "undefined")
2243         {
2244             if (box.a11yCaretOffset)
2245                 offset = box.a11yCaretOffset;
2246             else
2247                 box.a11yCaretOffset = offset = 0;
2248         }
2249         var startNode = node.getElementsByClassName("sourceRowText").item(0)
2250         if (startNode && startNode.firstChild && startNode.firstChild.nodeType == Node.TEXT_NODE)
2251         {
2252             startNode = startNode.firstChild;
2253             if (offset >= startNode.length)
2254                 box.a11yCaretOffset = offset = startNode.length - 1;
2255         }
2256         else
2257         {
2258             startNode = node; //offset is now the number of nodes, not characters within a text node
2259             offset = 1;
2260         }
2261         this.insertCaretToNode(panel, startNode, offset);
2262     },
2263 
2264     getCaretDetails: function(doc)
2265     {
2266         var sel = doc.defaultView.getSelection();
2267         return [sel.focusNode, sel.focusOffset];
2268     },
2269 
2270     onUpdateScriptLocation: function(panel, file)
2271     {
2272         var panelA11y = this.getPanelA11y(panel);
2273         if (!panelA11y)
2274             return;
2275 
2276         var box = panel.selectedSourceBox
2277         var viewport = panel.selectedSourceBox.getElementsByClassName("sourceViewport").item(0);
2278         box.tabIndex = -1;
2279         viewport.tabIndex = 0;
2280         viewport.setAttribute("role", "textbox");
2281         viewport.setAttribute("aria-multiline", "true");
2282         viewport.setAttribute("aria-readonly", "true");
2283         fileName = Url.getFileName(file.href);
2284         viewport.setAttribute("aria-label", Locale.$STRF("a11y.labels.source code for file",
2285             [fileName]));
2286         //bit ugly, but not sure how else I can get the caret into the sourcebox without a mouse
2287         var focusElem = Firebug.chrome.window.document.commandDispatcher.focusedElement;
2288         var line = box.getLineNode(box.firstViewableLine);
2289         if (!line)
2290             return;
2291 
2292         var node = line.getElementsByClassName("sourceRowText").item(0);
2293         this.insertCaretToNode(panel, node);
2294         // move focus back to where it was
2295         this.focus(focusElem);
2296     },
2297 
2298     insertCaretToNode: function(panel, node, startOffset)
2299     {
2300         if (!startOffset)
2301             startOffset = 0;
2302         var sel = panel.document.defaultView.getSelection();
2303         sel.removeAllRanges();
2304         var range = panel.document.createRange();
2305         range.setStart(node, startOffset);
2306         range.setEnd(node, startOffset);
2307         sel.addRange(range);
2308     },
2309 
2310     onScriptContextMenu: function(event)
2311     {
2312         if (event.button == 0) //i.e. keyboard, not right mouse click
2313         {
2314             //Try to find the line node based on the caret and manually trigger the context menu
2315             var panel = Firebug.getElementPanel(event.target);
2316             var panelA11y = this.getPanelA11y(panel);
2317             if (!panelA11y)
2318                 return;
2319 
2320             var sel = event.target.ownerDocument.defaultView.getSelection();
2321             var node = sel.focusNode.parentNode;
2322             var x = event.pageX
2323             if (x == 0)
2324             {
2325                 //TODO: This is ugly and way too inaccurate, how to get xy coordinates from selection object?
2326                 var charWidth = panelA11y.oneEmElem ? panelA11y.oneEmElem.clientWidth * 0.65 : 7.5;
2327                 x = node.offsetLeft + sel.focusOffset * charWidth;
2328             }
2329             var y = event.pageY;
2330             if (y >= event.target.clientHeight)
2331                y = node.offsetTop;
2332             Firebug.chrome.window.document.popupNode = node;
2333             Firebug.chrome.$("fbContextMenu").openPopup(node.ownerDocument.body, "overlap", x, y,
2334                 true);
2335             Events.cancelEvent(event);
2336         }
2337     },
2338 
2339     onWatchPanelRefreshed: function(panel)
2340     {
2341         var panelA11y = this.getPanelA11y(panel);
2342         if (!panelA11y)
2343             return;
2344 
2345         var watchEditTrigger = panel.panelNode.getElementsByClassName("watchEditCell").item(0);
2346         if (watchEditTrigger)
2347             this.makeFocusable(watchEditTrigger, true);
2348     },
2349 
2350     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2351     // Call Stack Panel
2352 
2353     onStackCreated: function(panel)
2354     {
2355         var panelA11y = this.getPanelA11y(panel);
2356         if (!panelA11y)
2357             return;
2358         var rows = panel.panelNode.getElementsByClassName("focusRow");
2359         Array.forEach(rows, function(e, i, a) {
2360             if ((panelA11y.lastIsDefault && i === rows.length - 1) ||
2361                 (!panelA11y.lastIsDefault && i === 0))
2362             {
2363                 this.setPanelTabStop(panel, e);
2364             }
2365             else
2366             {
2367                 this.makeFocusable(e, false);
2368             }
2369         }, this);
2370     },
2371 
2372     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2373     // Breakpoints Panel
2374 
2375     onBreakRowsRefreshed: function(panel, rootNode)
2376     {
2377         var rows = rootNode.getElementsByClassName("focusRow");
2378         for (var i = 0; i < rows.length; i++)
2379         {
2380             this.makeFocusable(rows[i], i == 0);
2381             if (i == 0)
2382                 this.setPanelTabStop(panel, rows[i]);
2383         }
2384         var groupHeaders = rootNode.getElementsByClassName("breakpointHeader");
2385         for (var i = 0; i < groupHeaders.length; i++)
2386         {
2387             var listBox = Dom.getNextByClass(groupHeaders[i], "breakpointsGroupListBox");
2388             if (listBox)
2389                 listBox.setAttribute("aria-label", groupHeaders[i].textContent);
2390         }
2391     },
2392 
2393     onScriptSearchMatchFound: function(panel, text, sourceBox, lineNo)
2394     {
2395         var panelA11y = this.getPanelA11y(panel);
2396         if (!panelA11y || !text)
2397             return;
2398 
2399         var matchFeedback = "";
2400         if (!sourceBox || lineNo === null)
2401         {
2402             matchFeedback = Locale.$STRF("a11y.updates.no matches found", [text]);
2403         }
2404         else
2405         {
2406             var line = sourceBox.getLine(panel.context, lineNo + 1);
2407             if (!line)
2408                 line = "";
2409             matchFeedback = Locale.$STRF("a11y.updates.match found for on line",
2410                 [text, lineNo + 1, Url.getFileName(sourceBox.href)]);
2411         }
2412         this.updateLiveElem(panel, matchFeedback, true);
2413     },
2414 
2415     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2416     // DOM Panel
2417 
2418     onMemberRowsAdded: function(panel, rows)
2419     {
2420         if (!panel)
2421             panel = Firebug.getElementPanel(rows[0]);
2422         var panelA11y = this.getPanelA11y(panel);
2423         if (!panelA11y || !rows)
2424             return;
2425 
2426         var setSize;
2427         var posInset;
2428         var setSize = rows.length;
2429         var posInset = 0;
2430         for (var i = 0; i < rows.length; i++)
2431         {
2432             var makeTab = (panelA11y.lastIsDefault && i === rows.length - 1) ||
2433                 (!panelA11y.lastIsDefault && i === 0)
2434             this.prepareMemberRow(panel, rows[i], makeTab, ++posInset, setSize);
2435         }
2436     },
2437 
2438     onMemberRowSliceAdded: function(panel, borderRows, posInSet, setSize)
2439     {
2440         if (!borderRows)
2441             return;
2442         var startRow = borderRows[0];
2443         var endRow = borderRows[1];
2444         if (!panel)
2445             panel = Firebug.getElementPanel(startRow);
2446         var panelA11y = this.getPanelA11y(panel);
2447         if (!panelA11y)
2448             return;
2449 
2450         var reFocusId = panelA11y.reFocusId;
2451         var row = startRow;
2452         do
2453         {
2454             this.prepareMemberRow(panel, row, false, posInSet++, setSize, reFocusId);
2455             if (row === endRow)
2456                 break;
2457         }
2458         while (row = row.nextSibling);
2459     },
2460 
2461     prepareMemberRow: function(panel, row, makeTab, posInSet, setSize, reFocusId)
2462     {
2463         var panelA11y = this.getPanelA11y(panel);
2464         if (!panelA11y|| !row)
2465             return;
2466 
2467         if (!row.cells[2])
2468             return;
2469 
2470         var cellChild = row.cells[2].firstChild;
2471         if (cellChild)
2472         {
2473             if (Css.hasClass(row, "hasChildren"))
2474                 cellChild.setAttribute("aria-expanded", Css.hasClass(row, "opened"));
2475             if (makeTab)
2476                 this.modifyPanelRow(panel, cellChild, true);
2477             cellChild.setAttribute("role", "treeitem");
2478             cellChild.setAttribute("aria-level", parseInt(row.getAttribute("level")) + 1);
2479             if (posInSet && setSize)
2480             {
2481                 cellChild.setAttribute("aria-setsize", setSize);
2482                 cellChild.setAttribute("aria-posinset", posInSet);
2483             }
2484             Css.setClass(cellChild, "focusRow");
2485             if (typeof reFocusId == "number" && row.rowIndex == reFocusId)
2486             {
2487                 this.modifyMemberRow(panel, cellChild, true);
2488                 this.focus(cellChild, true, true);
2489                 panelA11y.reFocusId = null;
2490             }
2491         }
2492     },
2493 
2494     modifyMemberRow: function(panel, row, inTabOrder)
2495     {
2496         var type = this.getObjectType(row)
2497         var labelCell = row.parentNode.previousSibling;
2498         row.setAttribute("aria-label", labelCell.textContent +
2499             ": " + " " + row.textContent + (type ? " (" + type + ")" : ""));
2500         if (inTabOrder)
2501             this.setPanelTabStop(panel, row);
2502         else
2503             this.makeFocusable(row, false);
2504     },
2505 
2506     onBeforeDomUpdateSelection: function (panel)
2507     {
2508         var panelA11y = this.getPanelA11y(panel);
2509         if (!panelA11y)
2510             return;
2511 
2512         var focusNode = panel.document.activeElement;
2513         if (this.isDirCell(focusNode))
2514             panelA11y.reFocusId = focusNode.parentNode.parentNode.rowIndex;
2515     },
2516 
2517     onWatchEndEditing: function(panel, row)
2518     {
2519         var panelA11y = this.getPanelA11y(panel);
2520         if (!panelA11y)
2521             return;
2522 
2523         panelA11y.reFocusId = 2;
2524     },
2525 
2526     onDomSearchMatchFound: function (panel, text, matchRow)
2527     {
2528         var panelA11y = this.getPanelA11y(panel);
2529         if (!panelA11y || !text)
2530             return;
2531 
2532         var matchFeedback = "";
2533         if (matchRow && matchRow.cells)
2534         {
2535             var dirCell = matchRow.getElementsByClassName("focusRow").item(0);
2536             if (dirCell)
2537             {
2538                 this.modifyPanelRow(panel, dirCell);
2539                 var rowLabel = dirCell.getAttribute("aria-label");
2540                 matchFeedback = Locale.$STRF("a11y.updates.match found in dom property",
2541                     [text, rowLabel]);
2542             }
2543         }
2544         else
2545         {
2546             matchFeedback = Locale.$STRF("a11y.updates.no matches found", [text]);
2547         }
2548         this.updateLiveElem(panel, matchFeedback, true);
2549     },
2550 
2551     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2552     // Net Panel
2553 
2554     isSubFocusRow: function(elem)
2555     {
2556         return Css.hasClass(elem, "focusRow") || Css.hasClass(elem, "wrappedText");
2557     },
2558 
2559     modifyNetRow: function(panel, row, inTabOrder)
2560     {
2561         var panelA11y = this.getPanelA11y(panel);
2562         if (!panelA11y || !row)
2563             return;
2564 
2565         if (this.isOuterFocusRow(row, true))
2566         {
2567             if (!Css.hasClass(row, "netInfoTabs"))
2568                 this.makeFocusable(row, inTabOrder);
2569             if ((Css.hasClass(row, "netRow") ||
2570                 Css.hasClass(row, "spyHeadTable")) && !row.hasAttribute("aria-expanded"))
2571             {
2572                 row.setAttribute("aria-expanded", Css.hasClass(row, "opened") + "");
2573             }
2574             var focusObjects = this.getFocusObjects(row);
2575             Array.forEach(focusObjects, function(e, i, a) {
2576                 this.makeFocusable(e);
2577                 if (Css.hasClass(e, "netTimeCol") && Dom.getAncestorByClass(e, "fromCache"))
2578                 {
2579                     e.setAttribute("aria-label", e.textContent + " (" +
2580                         Locale.$STR("a11y.labels.cached") +")");
2581                 }
2582             }, this);
2583         }
2584         else return;
2585     },
2586 
2587     getNetAncestorRow: function(elem, useSubRow)
2588     {
2589         return useSubRow ? Dom.getAncestorByClass(elem, "subFocusRow") ||
2590             Dom.getAncestorByClass(elem, "netRow") : Dom.getAncestorByClass(elem, "netRow");
2591     },
2592 
2593     onNetMouseDown: function(event)
2594     {
2595         var node = Dom.getAncestorByClass(event.target, "focusRow");
2596         if (node)
2597         {
2598             this.modifyPanelRow(Firebug.getElementPanel(node), node, false);
2599         }
2600         else
2601         {
2602             node = Dom.getAncestorByClass(event.target, "subFocusRow");
2603             if (!node)
2604                 return;
2605 
2606             var focusRow = node.getElementsByClassName("focusRow").item(0);
2607             if (!focusRow)
2608                 return;
2609 
2610             this.modifyPanelRow(Firebug.getElementPanel(focusRow), focusRow, false);
2611             this.focus(focusRow);
2612         }
2613     },
2614 
2615     onNetFocus: function(e) {
2616         var target = e.target;
2617         var panel = Firebug.getElementPanel(target);
2618         var panelA11y = this.getPanelA11y(panel);
2619         if (!panelA11y)
2620             return;
2621 
2622         if (!Css.hasClass(target, "netCol") && !Css.hasClass(target, "netHeaderCell"))
2623             return;
2624 
2625         if (Css.hasClass(target, "netHrefCol"))
2626         {
2627             var hrefLabel = target.getElementsByClassName("netHrefLabel").item(0);
2628             var fullHrefLabel = target.getElementsByClassName("netFullHrefLabel").item(0);
2629             if (hrefLabel && fullHrefLabel)
2630             {
2631                 Css.setClass(fullHrefLabel, "a11yShowFullLabel");
2632                 fullHrefLabel.style.marginTop = (hrefLabel.offsetHeight  + 4) + "px";
2633                 return;
2634             }
2635         }
2636         var rangeParent = Dom.getAncestorByClass(target, "netRow");
2637         var browser = Firebug.chrome.getPanelBrowser(panel);
2638         // these two lines are necessary, because otherwise the info tip will not have the correct
2639         // dimensions when it's positioned, and the contentscould be placed outside of Firebug's
2640         // viewport (making it impossible to read for keyboard users)
2641         // This will be called again in showInfoTip
2642         panel.showInfoTip(browser.infoTip, target, target.offsetLeft, target.offsetTop,
2643             rangeParent, 0);
2644         browser.infoTip.setAttribute("active", "true");
2645         var left = Css.hasClass(target, "netTimeCol") ?
2646             target.offsetLeft - browser.infoTip.offsetWidth - 12 :
2647             target.offsetLeft + target.offsetWidth - 4;
2648         Firebug.InfoTip.showInfoTip(browser.infoTip, panel, target, left,
2649             target.offsetTop - panel.panelNode.scrollTop - 12, rangeParent, 0);
2650     },
2651 
2652     onNetBlur: function(e) {
2653         var target = e.target;
2654         var panel = Firebug.getElementPanel(target);
2655         var panelA11y = this.getPanelA11y(panel);
2656         if (!panelA11y)
2657             return;
2658 
2659         if (Css.hasClass(target, "netHrefCol"))
2660         {
2661             var hrefLabel = target.getElementsByClassName("netHrefLabel").item(0);
2662             var fullHrefLabel = target.getElementsByClassName("netFullHrefLabel").item(0);
2663             if (hrefLabel && fullHrefLabel)
2664             {
2665                 Css.removeClass(fullHrefLabel, "a11yShowFullLabel");
2666                 fullHrefLabel.style.marginTop = "0px";
2667             }
2668         }
2669         var browser = Firebug.chrome.getPanelBrowser(panel);
2670         Firebug.InfoTip.hideInfoTip(browser.infoTip);
2671     },
2672 
2673     onNetMatchFound: function(panel, text, row)
2674     {
2675         //TODO localize for 1.5
2676         var panelA11y = this.getPanelA11y(panel);
2677         if (!panelA11y)
2678             return;
2679 
2680         var matchFeedback = "";
2681         if (!row)
2682         {
2683             matchFeedback = Locale.$STRF("a11y.updates.no matches found", [text]);
2684         }
2685         else
2686         {
2687             var foundWhere = "";
2688             var parentRow = Dom.getAncestorByClass(row, "netRow");
2689             if (!parentRow)
2690             {
2691                 parentRow = Dom.getAncestorByClass(row, "netInfoRow");
2692                 if (parentRow)
2693                     parentRow = parentRow.previousSibling;
2694             }
2695             if (Css.hasClass(row, "netHrefLabel"))
2696                 foundWhere = Locale.$STR("net.header.URL");
2697             else if (Css.hasClass(row, "netStatusLabel"))
2698                 foundWhere = Locale.$STR("net.header.Status");
2699             else if (Css.hasClass(row, "netDomainLabel"))
2700                 foundWhere = Locale.$STR("net.header.Domain");
2701             else if (Css.hasClass(row, "netSizeLabel"))
2702                 foundWhere = Locale.$STR("net.header.Size");
2703             else if (Css.hasClass(row, "netTimeLabel"))
2704                 foundWhere = Locale.$STR("net.header.Timeline");
2705             else
2706                 foundWhere = "request details";
2707             if (parentRow && parentRow.repObject)
2708             {
2709                 var file = parentRow.repObject;
2710                 var href =  (file.method ? file.method.toUpperCase() : "?") + " " +
2711                     Url.getFileName(file.href);
2712                 matchFeedback = Locale.$STRF("a11y.updates.match found in net row",
2713                     [text, href, foundWhere, row.textContent]);
2714             }
2715             else if (Dom.getAncestorByClass(row, "netSummaryRow"))
2716             {
2717                 matchFeedback = Locale.$STRF("a11y.updates.match found in net summary row",
2718                     [text, row.textContent]);
2719             }
2720         }
2721         this.updateLiveElem(panel, matchFeedback, true); //should not use alert
2722     },
2723 
2724     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2725     // Panel Navigation
2726 
2727     insertHiddenText: function(panel, elem, text, asLastNode, id)
2728     {
2729         var span = panel.document.createElement("span");
2730         span.className ="offScreen";
2731         span.textContent = text;
2732 
2733         if (id)
2734             span.id = id;
2735 
2736         if (asLastNode)
2737             elem.appendChild(span);
2738         else
2739             elem.insertBefore(span, elem.firstChild);
2740     },
2741 
2742     getLogRowType: function(elem)
2743     {
2744         var type = "";
2745         if (!elem)
2746             return type;
2747 
2748         var className = elem.className.match(/\logRow-(\w+)\b/);
2749         if (className)
2750             type = className[1];
2751 
2752         if (!type)
2753         {
2754             if (Css.hasClass(elem, "errorTitle"))
2755                 type = "detailed error";
2756             else if (Css.hasClass(elem, "errorSourceBox"))
2757                 type = "error source line";
2758             else
2759                 type = this.getObjectType(elem);
2760         }
2761 
2762         if (type == "stackFrame")
2763             type="";
2764 
2765         return type;
2766     },
2767 
2768     getObjectType: function(elem)
2769     {
2770         var type = "";
2771         if (elem.nodeName == "img")
2772             return type;
2773 
2774         var className = elem.className.match(/\bobject(Box|Link)-(\w+)/);
2775         if (className)
2776             type = className[2];
2777 
2778         switch (type)
2779         {
2780             case "null":
2781             case "undefined":
2782                 type = "";
2783                 break;
2784 
2785             case "number":
2786                 if (elem.textContent == "true" || elem.textContent == "false")
2787                     type = "boolean";
2788 
2789             case "":
2790             case "object":
2791                 if (elem.repObject)
2792                 {
2793                     try
2794                     {
2795                         var obj = elem.repObject;
2796                         if (!obj)
2797                             return type;
2798 
2799                         type = typeof obj;
2800                         if (obj instanceof Array)
2801                             type = "array";
2802 
2803                         if (typeof obj.lineNo != "undefined")
2804                             type = "function call";
2805                     }
2806                     catch(e) {}
2807                 }
2808         }
2809 
2810         return type;
2811     },
2812 
2813     modifyPanelRow: function (panel, row, inTabOrder)
2814     {
2815         if (Css.hasClass(row, "a11yModified"))
2816             return;
2817 
2818         var panelA11y = this.getPanelA11y(panel);
2819         if (!panelA11y || !row)
2820             return;
2821 
2822         switch (panelA11y.type)
2823         {
2824             case "console":
2825                 this.modifyConsoleRow(panel,row, inTabOrder);
2826                 break;
2827 
2828             case "css":
2829                 this.modifyCSSRow(panel, row, inTabOrder);
2830                 break;
2831 
2832             case "net":
2833                 this.modifyNetRow(panel, row, inTabOrder);
2834                 break;
2835         }
2836         Css.setClass(row, "a11yModified");
2837     },
2838 
2839     focusSiblingRow: function(panel, target, goUp)
2840     {
2841         var newRow = this[goUp ? "getPreviousByClass" : "getNextByClass"](target, "focusRow",
2842             true, panel.panelNode)
2843         if (!newRow)
2844             return;
2845 
2846         this.focusPanelRow(panel, newRow)
2847     },
2848 
2849     focusPageSiblingRow: function(panel, target, goUp)
2850     {
2851         var rows = this.getFocusRows(panel);
2852         var index = this.getRowIndex(rows, target);
2853         var newRow = this.getValidRow(rows, goUp ? index - 10 : index + 10);
2854         this.focusPanelRow(panel, newRow);
2855     },
2856 
2857     focusEdgeRow: function(panel, target, goUp)
2858     {
2859         var rows = this.getFocusRows(panel);
2860         var newRow = this.getValidRow(rows, goUp ? 0 : rows.length -1);
2861         this.focusPanelRow(panel, newRow);
2862     },
2863 
2864     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2865     // Utils
2866 
2867     onPanelFocus: function(event)
2868     {
2869         var panel = Firebug.getElementPanel(event.target);
2870         var panelA11y = this.getPanelA11y(panel);
2871         if (!panelA11y)
2872             return;
2873 
2874         var target = event.target;
2875         if (this.isTabWorthy(target) && target !== this.getPanelTabStop(panel))
2876             this.setPanelTabStop(panel, target);
2877         if (target.getAttribute("role").match(/gridcell|rowheader|columnheader/))
2878         {
2879             var cell = (target.nodeName.toLowerCase() == "td" ||
2880                 (target.nodeName.toLowerCase() == "th" ? target : target.parentNode));
2881             panelA11y.cellIndex = (cell.cellIndex !== undefined ? cell.cellIndex : undefined);
2882         }
2883         else
2884         {
2885             if (Css.hasClass(target, "netInfoTab"))
2886                 this.dispatchMouseEvent(target, "click");
2887             panelA11y.cellIndex = undefined; //reset if no longer in grid
2888         }
2889     },
2890 
2891     getFocusRows: function(panel)
2892     {
2893         var nodes = panel.panelNode.getElementsByClassName("focusRow");
2894         return Array.filter(nodes, function(e, i, a)
2895         {
2896             return this.isVisibleByStyle(e) && Xml.isVisible(e);
2897         }, this);
2898     },
2899 
2900     getLastFocusChild: function(target)
2901     {
2902         var focusChildren = target.getElementsByClassName("focusRow");
2903         return focusChildren.length > 0 ? focusChildren[focusChildren.length -1] : null;
2904     },
2905 
2906     getFirstFocusChild: function(target)
2907     {
2908         var focusChildren = target.getElementsByClassName("focusRow");
2909         return focusChildren.length > 0 ? focusChildren[0] : null;
2910     },
2911 
2912     focus: function(elem, noVisiCheck, needsMoreTime)
2913     {
2914         if (Dom.isElement(elem) && (noVisiCheck || this.isVisibleByStyle(elem)))
2915         {
2916             Firebug.currentContext.setTimeout(function() {
2917                     elem.focus()
2918                 }, needsMoreTime ? 500 : 10
2919             );
2920         }
2921     },
2922 
2923     makeFocusable: function(elem, inTabOrder)
2924     {
2925         if (elem)
2926             elem.setAttribute("tabindex", inTabOrder ? "0" : "-1");
2927     },
2928 
2929     makeUnfocusable: function(elem)
2930     {
2931         if (elem)
2932             elem.removeAttribute("tabindex");
2933     },
2934 
2935     reportFocus: function(event)
2936     {
2937         FBTrace.sysout("focus: " + event.target.nodeName + "#" + event.target.id + "." +
2938             event.target.className, event.target);
2939     },
2940 
2941     dispatchMouseEvent: function (node, eventType, clientX, clientY, button)
2942     {
2943         if (!clientX)
2944             clientX = 0;
2945         if (!clientY)
2946             clientY = 0;
2947         if (!button)
2948             button = 0;
2949         if (typeof node == "string")
2950             throw new Error("a11y.dispatchMouseEvent obsolete API");
2951         var doc = node.ownerDocument;
2952         var event = doc.createEvent("MouseEvents");
2953         event.initMouseEvent(eventType, true, true, doc.defaultView,
2954             0, 0, 0, clientX, clientY, false, false, false, false, button, null);
2955         node.dispatchEvent(event);
2956     },
2957 
2958     isVisibleByStyle: function (elem)
2959     {
2960         if (!elem || elem.nodeType != Node.ELEMENT_NODE)
2961             return false;
2962         var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
2963         return style.visibility !== "hidden" && style.display !== "none";
2964     },
2965 
2966     isTabWorthy: function (elem)
2967     {
2968         return this.isFocusRow(elem) || this.isFocusObject(elem);
2969     },
2970 
2971     isOuterFocusRow: function(elem, includeSubRow)
2972     {
2973         return includeSubRow ? this.isSubFocusRow(elem) : Css.hasClass(elem, "outerFocusRow");
2974     },
2975 
2976     isProfileRow: function(elem)
2977     {
2978         return Css.hasClass(elem, "profileRow");
2979     },
2980 
2981     isFocusRow: function(elem)
2982     {
2983         return Css.hasClass(elem, "focusRow");
2984     },
2985 
2986     isFocusObject: function(elem)
2987     {
2988         return Css.hasClass(elem, "a11yFocus");
2989     },
2990 
2991     isFocusNoTabObject: function(elem)
2992     {
2993         return Css.hasClass(elem, "a11yFocusNoTab");
2994     },
2995 
2996     isDirCell: function(elem)
2997     {
2998         return Css.hasClass(elem.parentNode, "memberValueCell");
2999     },
3000 
3001     panelHasFocus: function(panel)
3002     {
3003         if (!panel || !panel.context)
3004             return false;
3005         var focusedElement = Firebug.chrome.window.document.commandDispatcher.focusedElement;
3006         var focusedPanel = Firebug.getElementPanel(focusedElement)
3007         return focusedPanel && (focusedPanel.name == panel.name);
3008     },
3009 
3010     getPanelA11y: function(panel, create)
3011     {
3012         var a11yPanels, panelA11y;
3013         if (!this.isEnabled() || !panel || !panel.name || !panel.context)
3014             return false;
3015 
3016         a11yPanels = panel.context.a11yPanels;
3017         if (!a11yPanels)
3018             a11yPanels = panel.context.a11yPanels = {};
3019         panelA11y = a11yPanels[panel.name];
3020         if (!panelA11y)
3021         {
3022             if (create)
3023                 panelA11y = a11yPanels[panel.name] = {};
3024             else
3025                 return false;
3026         }
3027         return panelA11y
3028     },
3029 
3030     // These utils are almost the same as their DOM namesakes,
3031     // except that the routine skips invisible containers
3032     // (rather than wasting time on their child nodes)
3033     getPreviousByClass: function (node, className, downOnly, maxRoot)
3034     {
3035         if (!node)
3036             return null;
3037 
3038         function criteria(node) {
3039             return node.nodeType == Node.ELEMENT_NODE && Css.hasClass(node, className);
3040         }
3041 
3042         for (var sib = node.previousSibling; sib; sib = sib.previousSibling)
3043         {
3044             if (!this.isVisibleByStyle(sib) || !Xml.isVisible(sib))
3045                 continue;
3046 
3047             var prev = this.findPreviousUp(sib, criteria);
3048             if (prev)
3049                 return prev;
3050 
3051             if (criteria(sib))
3052                 return sib;
3053         }
3054         if (!downOnly)
3055         {
3056             var next = this.findPreviousUp(node, criteria);
3057             if (next)
3058                 return next;
3059         }
3060         if (node.parentNode && node.parentNode != maxRoot)
3061         {
3062             if (criteria(node.parentNode))
3063                 return node.parentNode;
3064             return this.getPreviousByClass(node.parentNode, className, true);
3065         }
3066     },
3067 
3068     getNextByClass: function (node, className, upOnly, maxRoot)
3069     {
3070         if (!node)
3071             return null;
3072 
3073         function criteria(node) {
3074             return node.nodeType == Node.ELEMENT_NODE && Css.hasClass(node, className);
3075         }
3076 
3077         if (!upOnly)
3078         {
3079             var next = this.findNextDown(node, criteria);
3080             if (next)
3081                 return next;
3082         }
3083 
3084         for (var sib = node.nextSibling; sib; sib = sib.nextSibling)
3085         {
3086             if (!this.isVisibleByStyle(sib) || !Xml.isVisible(sib))
3087                 continue;
3088             if (criteria(sib))
3089                 return sib;
3090 
3091             var next = this.findNextDown(sib, criteria);
3092             if (next)
3093                 return next;
3094         }
3095 
3096         if (node.parentNode && node.parentNode != maxRoot)
3097             return this.getNextByClass(node.parentNode, className, true);
3098     },
3099 
3100     findNextDown: function(node, criteria)
3101     {
3102         if (!node)
3103             return null;
3104 
3105         for (var child = node.firstChild; child; child = child.nextSibling)
3106         {
3107             if (!this.isVisibleByStyle(child) || !Xml.isVisible(child))
3108                 continue;
3109 
3110             if (criteria(child))
3111                 return child;
3112 
3113             var next = this.findNextDown(child, criteria);
3114             if (next)
3115                 return next;
3116         }
3117     },
3118 
3119     findPreviousUp: function(node, criteria)
3120     {
3121         if (!node)
3122             return null;
3123 
3124         for (var child = node.lastChild; child; child = child.previousSibling)
3125         {
3126             if (!this.isVisibleByStyle(child) || !Xml.isVisible(child))
3127                 continue;
3128 
3129             var next = this.findPreviousUp(child, criteria);
3130             if (next)
3131                 return next;
3132 
3133             if (criteria(child))
3134                 return child;
3135         }
3136     }
3137 });
3138 
3139 // ************************************************************************************************
3140 // Registration
3141 
3142 Firebug.registerModule(Firebug.A11yModel);
3143 
3144 return Firebug.A11yModel;
3145 
3146 // ************************************************************************************************
3147 });
3148