1 /* See license.txt for terms of usage */
  2 FBL.ns(function() { with (FBL) {
  3 var singleSpaceTag = DIV({'class' : 'a11y1emSize'}, "x");
  4 //************************************************************************************************
  5 //Module Management
  6 Firebug.A11yModel = extend(Firebug.Module,
  7 {
  8     dispatchName: "a11y",
  9 
 10     initialize : function()
 11     {
 12         this.handleTabBarFocus = bind(this.handleTabBarFocus, this);
 13         this.handleTabBarBlur = bind(this.handleTabBarBlur, this);
 14         this.handlePanelBarKeyPress = bind(this.handlePanelBarKeyPress, this);
 15         this.onNavigablePanelKeyPress = bind(this.onNavigablePanelKeyPress, this);
 16         this.onConsoleMouseDown = bind(this.onConsoleMouseDown, this);
 17         this.onLayoutKeyPress = bind(this.onLayoutKeyPress, this);
 18         this.onCSSKeyPress = bind(this.onCSSKeyPress, this);
 19         this.onCSSMouseDown = bind(this.onCSSMouseDown, this);
 20         this.onHTMLKeyPress = bind(this.onHTMLKeyPress, this);
 21         this.onHTMLFocus = bind(this.onHTMLFocus, this);
 22         this.onHTMLBlur = bind(this.onHTMLBlur, this);
 23         this.onPanelFocus = bind(this.onPanelFocus, this);
 24         this.onLayoutFocus = bind(this.onLayoutFocus, this);
 25         this.onLayoutBlur = bind(this.onLayoutBlur, this);
 26         this.onScriptContextMenu = bind(this.onScriptContextMenu, this);
 27         this.onCSSPanelContextMenu = bind(this.onCSSPanelContextMenu, this);
 28         this.onScriptKeyPress = bind(this.onScriptKeyPress, this);
 29         this.onScriptKeyUp = bind(this.onScriptKeyUp, this);
 30         this.onScriptMouseUp = bind(this.onScriptMouseUp, this);
 31         this.onNetKeyPress = bind(this.onNetKeyPress, this);
 32         this.onNetMouseDown = bind(this.onNetMouseDown, this);
 33         this.onNetFocus = bind(this.onNetFocus, this);
 34         this.onNetBlur = bind(this.onNetBlur, this);
 35         Firebug.chrome.window.a11yEnabled = false; // mark ourselves disabled so we don't performDisable() if we are not enabled.
 36         Firebug.Debugger.addListener(this);
 37     },
 38 
 39     initializeUI : function()
 40     {
 41         //Initialize according to the current pref value.
 42         this.updateOption("a11y.enable", this.isEnabled());
 43     },
 44 
 45     isEnabled : function()
 46     {
 47         return Firebug.getPref("extensions.firebug", "a11y.enable");
 48     },
 49 
 50     updateOption: function(name, value)
 51     {
 52         if (FBTrace.DBG_A11Y)
 53             FBTrace.sysout("a11y.updateOption; " + name + ": " + value +
 54                 ", Current chrome: " + Firebug.chrome.getName() +
 55                 ", Original chrome: " + Firebug.originalChrome.getName());
 56         if (name == "a11y.enable")
 57         {
 58             // Update for current chrome
 59             this.set(value, Firebug.chrome);
 60             // If the current chrome is external window, update also original chrome.
 61             if (Firebug.chrome != Firebug.originalChrome)
 62             {
 63                 this.set(value, Firebug.originalChrome);
 64                 if (FBTrace.DBG_A11Y)
 65                     FBTrace.sysout("a11y.updateOption; (original chrome)");
 66             }
 67         }
 68     },
 69 
 70     reattachContext : function(browser, context)
 71     {
 72         if (FBTrace.DBG_A11Y)
 73             FBTrace.sysout("a11y.reattachContext; " + this.isEnabled() + ", " +
 74                 Firebug.chrome.getName());
 75         if (this.isEnabled())
 76             this.set(true, Firebug.chrome);
 77     },
 78 
 79     set : function(enable, chrome)
 80     {
 81         if (chrome.window.a11yEnabled == enable)
 82             return;
 83         if (enable)
 84             this.performEnable(chrome);
 85         else
 86             this.performDisable(chrome);
 87         chrome.window.a11yEnabled = enable;
 88     },
 89 
 90     performEnable : function(chrome)
 91     {
 92         var tmpElem;
 93         //add class used by all a11y related css styles (e.g. :focus and -moz-user-focus styles)
 94         setClass(chrome.$('fbContentBox'), 'useA11y');
 95         setClass(chrome.$('fbStatusBar'), 'useA11y');
 96         tmpElem = chrome.$('fbStatusPrefix');
 97         if (tmpElem) tmpElem.setAttribute('value', $STR("a11y.labels.firebug status"));
 98         
 99         //manage all key events in toolbox (including tablists)
100         tmpElem = chrome.$("fbContentBox");
101         if (tmpElem) tmpElem.addEventListener("keypress", this.handlePanelBarKeyPress , true);
102         //make focus stick to inspect button when clicked
103         tmpElem = chrome.$("fbInspectButton");
104         if (tmpElem) tmpElem.addEventListener("mousedown", this.focusTarget, true);
105         tmpElem = chrome.$('fbPanelBar1-panelTabs');
106         if (tmpElem) tmpElem.addEventListener('focus', this.handleTabBarFocus, true);
107         tmpElem = chrome.$('fbPanelBar1-panelTabs');
108         if (tmpElem) tmpElem.addEventListener('blur', this.handleTabBarBlur, true);
109         tmpElem = chrome.$('fbPanelBar2-panelTabs');
110         if (tmpElem) tmpElem.addEventListener('focus', this.handleTabBarFocus, true);
111         tmpELem = chrome.$('fbPanelBar2-panelTabs');
112         if (tmpElem) tmpElem.addEventListener('blur', this.handleTabBarBlur, true);
113         tmpElem = chrome.$("fbPanelBar1");
114         if (tmpElem) setClass(tmpElem.browser.contentDocument.body, 'useA11y');
115         tmpElem = chrome.$("fbPanelBar2");
116         if (tmpElem) setClass(tmpElem.browser.contentDocument.body, 'useA11y');
117         Firebug.Editor.addListener(this);
118     },
119 
120     performDisable : function(chrome)
121     {
122         var tmpElem;
123         //undo everything we did in performEnable
124         removeClass(chrome.$('fbContentBox'), 'useA11y');
125         removeClass(chrome.$('fbStatusBar'), 'useA11y');
126         tmpElem = chrome.$("fbPanelBar1");
127         if (tmpElem) tmpElem.removeEventListener("keypress", this.handlePanelBarKeyPress , true);
128         tmpElem = chrome.$("fbInspectButton");
129         if (tmpElem) tmpElem.removeEventListener("mousedown", this.focusTarget, true);
130         tmpElem = chrome.$('fbPanelBar1-panelTabs');
131         if (tmpElem) tmpElem.removeEventListener('focus', this.handleTabBarFocus, true);
132         tmpElem = chrome.$('fbPanelBar1-panelTabs')
133         if (tmpElem) tmpElem.removeEventListener('blur', this.handleTabBarBlur, true);
134         tmpElem = chrome.$('fbPanelBar2-panelTabs');
135         if (tmpElem) tmpElem.removeEventListener('focus', this.handleTabBarFocus, true);
136         tmpElem = chrome.$('fbPanelBar2-panelTabs');
137         if (tmpElem) tmpElem.removeEventListener('blur', this.handleTabBarBlur, true);
138         tmpElem = chrome.$("fbPanelBar1");
139         if (tmpElem) 
140         {
141             removeClass(tmpElem.browser.contentDocument.body, 'useA11y');
142             tmpElem.browser.setAttribute('showcaret', false);
143         }
144         tmpElem = chrome.$("fbPanelBar2");
145         if (tmpElem) removeClass(tmpElem.browser.contentDocument.body, 'useA11y');
146         Firebug.Editor.removeListener(this);
147     },
148 
149     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
150     // Context & Panel Management
151 
152     onInitializeNode : function(panel, actAsPanel)
153     {
154         var panelA11y = this.getPanelA11y(panel, true);
155         if (!panelA11y)
156             return;
157         panelA11y.tabStop = null;
158         panelA11y.manageFocus = false;
159         panelA11y.lastIsDefault = false;
160         actAsPanel = actAsPanel ? actAsPanel : panel.name;
161         panelA11y.type = actAsPanel;
162         //panel.context.chrome.$("fbContentBox").addEventListener("focus", this.reportFocus, true);
163         this.makeFocusable(panel.panelNode, false);
164         switch (panelA11y.type)
165         {
166             case 'console':
167                 panelA11y.manageFocus = true;
168                 if (panel.name == "console")
169                 {
170                     panel.panelNode.setAttribute('aria-label', $STR('a11y.labels.log rows'));
171                     panelA11y.lastIsDefault = true;
172                     panel.panelNode.setAttribute('role', 'list');
173                 }
174                 else if (panel.name == "callstack")
175                 {
176                     panel.panelNode.setAttribute('role', 'list');
177                     panel.panelNode.setAttribute('aria-label', $STR('a11y.labels.call stack'));
178                 }
179                 else
180                     panel.panelNode.setAttribute('role', 'presentation');
181                 //panel.panelNode.setAttribute('aria-live', 'polite');
182                 panel.panelNode.addEventListener("keypress", this.onNavigablePanelKeyPress, false);
183                 panel.panelNode.addEventListener("focus", this.onPanelFocus, true);
184                 panel.panelNode.addEventListener("mousedown", this.onConsoleMouseDown, false);
185                 if (panel.name == "breakpoints")
186                     panel.panelNode.style.overflowX = "hidden";
187                 break;
188             case 'html':
189                 panel.panelNode.setAttribute('role', 'tree');
190                 panel.panelNode.setAttribute('aria-label', $STR('a11y.labels.document structue'));
191                 panel.panelNode.addEventListener("keypress", this.onHTMLKeyPress, false);
192                 panel.panelNode.addEventListener("focus", this.onHTMLFocus, true);
193                 panel.panelNode.addEventListener("blur", this.onHTMLBlur, true);
194                 break;
195             case 'css':
196                 panelA11y.manageFocus = true;
197                 panel.panelNode.addEventListener("keypress", this.onCSSKeyPress, false);
198                 panel.panelNode.addEventListener("mousedown", this.onCSSMouseDown, false);
199                 panel.panelNode.addEventListener("focus", this.onPanelFocus, true);
200                 panel.panelNode.addEventListener('contextmenu', this.onCSSPanelContextMenu, false)
201                 this.insertHiddenText(panel, panel.panelNode, $STR('a11y.labels.overridden'), false, "CSSOverriddenDescription");
202                 panel.panelNode.setAttribute('role', panel.name == "stylesheet" ? 'list' : "presentation");
203                 break;
204             case 'layout':
205                 panelA11y.manageFocus = true;
206                 panel.panelNode.addEventListener("keypress", this.onLayoutKeyPress, false);
207                 panel.panelNode.addEventListener("focus", this.onLayoutFocus, true);
208                 panel.panelNode.addEventListener("blur", this.onLayoutBlur, true);
209                 break;
210             case 'script':
211                 panel.panelNode.addEventListener('contextmenu', this.onScriptContextMenu, true);
212                 panel.panelNode.addEventListener('keypress', this.onScriptKeyPress, true);
213                 panel.panelNode.addEventListener('keyup', this.onScriptKeyUp, true);
214                 panel.panelNode.addEventListener('mouseup', this.onScriptMouseUp, true);
215                 panelA11y.oneEmElem = this.addSingleSpaceElem(panel.panelNode);
216                 break;
217             case 'net':
218                 panelA11y.manageFocus = true;
219                 panel.panelNode.addEventListener("keypress", this.onNavigablePanelKeyPress, false);
220                 panel.panelNode.addEventListener("focus", this.onPanelFocus, true);
221                 panel.panelNode.addEventListener("focus", this.onNetFocus, true);
222                 panel.panelNode.addEventListener("blur", this.onNetBlur, true);
223                 panel.panelNode.addEventListener("mousedown", this.onNetMouseDown, false);
224                 break;
225         }
226     },
227 
228     onDestroyNode : function(panel, actAsPanel)
229     {
230         var panelA11y = this.getPanelA11y(panel);
231         if (!panelA11y)
232             return;
233         panelA11y = null;
234         actAsPanel = actAsPanel ? actAsPanel : panel.name;
235         //remove all event handlers we added in onInitializeNode
236         switch (actAsPanel)
237         {
238             case 'console':
239                 panel.panelNode.removeEventListener("keypress", this.onNavigablePanelKeyPress, false);
240                 panel.panelNode.removeEventListener("focus", this.onPanelFocus, true);
241                 panel.panelNode.removeEventListener("mousedown", this.onConsoleMouseDown, false);
242                 break;
243             case 'html':
244                 panel.panelNode.removeEventListener("keypress", this.onHTMLKeyPress, false);
245                 panel.panelNode.removeEventListener("focus", this.onHTMLFocus, true);
246                 panel.panelNode.removeEventListener("blur", this.onHTMLBlur, true);
247                 break;
248             case 'css':
249                 panel.panelNode.removeEventListener("keypress", this.onCSSKeyPress, false);
250                 panel.panelNode.removeEventListener("mousedown", this.onCSSMouseDown, false);
251                 panel.panelNode.removeEventListener("focus", this.onPanelFocus, true);
252                 panel.panelNode.removeEventListener("blur", this.onPanelBlur, true);
253                 panel.panelNode.removeEventListener('contextmenu', this.onCSSPanelContextMenu, false)
254                 break;
255             case 'layout':
256                 panel.panelNode.removeEventListener("keypress", this.onLayoutKeyPress, false);
257                 panel.panelNode.removeEventListener("focus", this.onLayoutFocus, true);
258                 panel.panelNode.removeEventListener("blur", this.onLayoutBlur, true);
259                 break;
260             case 'script':
261                 panel.panelNode.removeEventListener('contextmenu', this.onScriptContextMenu, true);
262                 panel.panelNode.removeEventListener('keypress', this.onScriptKeyPress, true);
263                 panel.panelNode.removeEventListener('keyup', this.onScriptKeyUp, true);
264                 panel.panelNode.removeEventListener('mouseup', this.onScriptMouseUp, true)
265                 break;
266             case 'net':
267                 panel.panelNode.removeEventListener("keypress", this.onNavigablePanelKeyPress, false);
268                 panel.panelNode.removeEventListener("focus", this.onPanelFocus, true);
269                 panel.panelNode.removeEventListener("focus", this.onNetFocus, true);
270                 panel.panelNode.removeEventListener("blur", this.onNetBlur, true);
271                 panel.panelNode.removeEventListener("mousedown", this.onNetMouseDown, false);
272                 break;
273         }
274     },
275 
276     showPanel : function(browser, panel)
277     {
278         var panelA11y = this.getPanelA11y(panel);
279         if (!panelA11y)
280             return;
281         var title = panel.name;
282         var panelType = Firebug.getPanelType(panel.name);
283         if (panelType)
284             title = Firebug.getPanelTitle(panelType);
285         Firebug.chrome.$('fbToolbar').setAttribute('aria-label', title + " " + $STR("a11y.labels.panel tools"))
286         var panelBrowser = Firebug.chrome.getPanelBrowser(panel);
287         panelBrowser.setAttribute('showcaret', (panel.name == "script"));
288         panelBrowser.contentDocument.body.setAttribute('aria-label', $STRF("a11y.labels.title panel", [title]));
289     },
290 
291     showSidePanel : function(browser, sidePanel)
292     {
293         var panelA11y = this.getPanelA11y(sidePanel);
294         if (!panelA11y)
295             return;
296         var panelBrowser = Firebug.chrome.getPanelBrowser(sidePanel);
297         var panelType = Firebug.getPanelType(sidePanel.name);
298         if (panelType)
299             title = Firebug.getPanelTitle(panelType);
300             panelBrowser.contentDocument.body.setAttribute('aria-label', $STRF("a11y.labels.title side panel", [title]));
301     },
302 
303     addLiveElem : function(panel, role, politeness)
304     {
305         var panelA11y = this.getPanelA11y(panel);
306         if (!panelA11y)
307             return;
308         if (panelA11y.liveElem && isElement(panelA11y.liveElem))
309             return;
310         var attrName = attrValue = "";
311         if (role)
312         {
313             attrName = 'role';
314             attrValue = role;
315         }
316         else
317         {
318             attrName = "aria-live";
319             attrValue = politeness ? politeness : 'polite';
320         }
321         var elem = panel.document.createElement('div');
322         elem.setAttribute(attrName, attrValue);
323         elem.className = "offScreen";
324         panel.document.body.appendChild(elem);
325         panelA11y.liveElem = elem;
326         return elem;
327     },
328 
329     updateLiveElem: function(panel, msg, useAlert)
330     {
331         var panelA11y = this.getPanelA11y(panel);
332         if (!panelA11y)
333             return;
334         var elem = panelA11y.liveElem;
335         if (!elem)
336             elem = this.addLiveElem(panel);
337         elem.textContent = msg;
338         if (useAlert)
339             elem.setAttribute('role', 'alert');
340     },
341 
342     addSingleSpaceElem : function(parent)
343     {
344         return singleSpaceTag.append({}, parent, this);
345     },
346 
347     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
348     // Toolbars & Tablists
349 
350     focusTarget : function(event)
351     {
352         this.focus(event.target);
353     },
354 
355     handlePanelBarKeyPress : function (event)
356     {
357         var target = event.originalTarget;
358         var isTab = target.nodeName.toLowerCase() == "paneltab";
359         var isButton = target.nodeName.search(/(xul:)?((toolbar)?button)|(checkbox)/) != -1;
360         var isDropDownMenu = isButton && (target.getAttribute('type') == "menu" || target.id == "fbLocationList") ;
361         var siblingTab, forward, toolbar, buttons;
362         var keyCode = event.keyCode || (event.type=='keypress' ? event.charCode : null);
363         if (keyCode == KeyEvent.DOM_VK_TAB)
364             this.ensurePanelTabStops(); //TODO: need a better solution to prevent loss of panel tabstop
365         if (isTab || isButton )
366         {
367             switch (keyCode)
368             {
369                 case KeyEvent.DOM_VK_LEFT:
370                 case KeyEvent.DOM_VK_RIGHT:
371                 case KeyEvent.DOM_VK_UP:
372                 case KeyEvent.DOM_VK_DOWN:
373                     forward = event.keyCode == KeyEvent.DOM_VK_RIGHT || event.keyCode == KeyEvent.DOM_VK_DOWN;
374                     if (isTab)
375                     {
376                         //will only work as long as long as siblings only consist of paneltab elements
377                         siblingTab = target[forward ? 'nextSibling' : 'previousSibling'];
378                         if (!siblingTab)
379                             siblingTab = target.parentNode[forward ? 'firstChild' : 'lastChild'];
380                         if (siblingTab)
381                         {
382                             var panelBar = getAncestorByClass(target, 'panelBar')
383                             setTimeout(bindFixed(function()
384                             {
385                                 panelBar.selectTab(siblingTab);
386                                 this.focus(siblingTab);
387                             }, this));
388                         }
389                    }
390                    else if (isButton)
391                    {
392                        if (target.id=="fbFirebugMenu" && !forward)
393                        {
394                             cancelEvent(event);
395                             return;
396                        }
397                        toolbar = getAncestorByClass(target, 'innerToolbar');
398                        if (toolbar)
399                        {
400                            var doc = target.ownerDocument;
401                            //temporarily make all buttons in the toolbar part of the tab order,
402                            //to allow smooth, native focus advancement
403                            setClass(toolbar, 'hasTabOrder');
404                            setTimeout(bindFixed(function() // time out needed to fix this behavior in 3.6
405                            {
406                                doc.commandDispatcher[forward ? 'advanceFocus' : 'rewindFocus']();
407                                //remove the buttons from the tab order again, so that it will remain uncluttered
408                                //Very ugly hack, but it works well. This prevents focus to 'spill out' of a
409                                //toolbar when using the left and right arrow keys
410                                if (!isAncestor(doc.commandDispatcher.focusedElement, toolbar))
411                                {
412                                    //we moved focus to somewhere out of the toolbar: not good. Move it back to where it was.
413                                    doc.commandDispatcher[!forward ? 'advanceFocus' : 'rewindFocus']();
414                                }
415                                removeClass(toolbar, 'hasTabOrder');
416                            }, this));
417 
418                        }
419                         cancelEvent(event);
420                         return;
421                    }
422                 break;
423                 case KeyEvent.DOM_VK_RETURN:
424                 case KeyEvent.DOM_VK_SPACE:
425                     if (isTab && target.tabMenu)
426                         target.tabMenu.popup.showPopup(target.tabMenu, -1, -1, "popup", "bottomleft", "topleft");
427                     else if (isButton)
428                     {
429                         if (isDropDownMenu)
430                         {
431                             if (target.id == "fbLocationList")
432                                 target.showPopup();
433                             else
434                                 target.open = true;
435                             cancelEvent(event);
436                             return false;
437                         }
438                     }
439                 break;
440                 case KeyEvent.DOM_VK_F4:
441                     if (isTab && target.tabMenu)
442                         target.tabMenu.popup.showPopup(target.tabMenu, -1, -1, "popup", "bottomleft", "topleft");
443                 break;
444             }
445         }
446     },
447 
448     handleTabBarFocus: function(event)
449     {
450         this.tabFocused = true;
451     },
452 
453     handleTabBarBlur: function(event)
454     {
455         this.tabFocused = false;
456     },
457 
458     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
459     // Panel Focus & Tab Order Management
460 
461     getPanelTabStop : function(panel)
462     {
463         var panelA11y = this.getPanelA11y(panel);
464         if (panelA11y)
465             return panelA11y.tabStop;
466         if (FBTrace.DBG_ERRORS)
467             FBTrace.sysout("a11y.getPanelTabStop null panel.context");
468         return null;
469     },
470 
471     ensurePanelTabStops: function()
472     {
473         if (!FirebugContext || !FirebugContext.chrome)
474             return;
475         var panel = Firebug.chrome.getSelectedPanel();
476         var sidePanel = Firebug.chrome.getSelectedSidePanel();
477         this.ensurePanelTabStop(panel);
478         if (sidePanel)
479             this.ensurePanelTabStop(sidePanel);
480     },
481 
482     ensurePanelTabStop: function(panel)
483     {
484         var panelA11y = this.getPanelA11y(panel);
485         if (!panelA11y)
486             return;
487         if (panelA11y.manageFocus)
488         {
489             var tabStop = this.getPanelTabStop(panel);
490             if (!tabStop || !this.isVisibleByStyle(tabStop) || !isVisible(tabStop))
491             {
492                 this.tabStop = null;
493                 this.findPanelTabStop(panel, 'focusRow', panelA11y.lastIsDefault);
494             }
495             else if (tabStop.getAttribute('tabindex') !== "0")
496                 tabStop.setAttribute('tabindex', "0");
497         if (tabStop)
498             this.checkModifiedState(panel, tabStop, true);
499         }
500     },
501 
502     checkModifiedState : function(panel, elem, makeTab)
503     {
504         var panelA11y = this.getPanelA11y(panel);
505         if (!panelA11y || !elem)
506             return;
507         switch (panelA11y.type)
508         {
509             case  'console' :
510                 if (hasClass(elem, 'focusRow'))
511                     this.modifyPanelRow(panel, elem, makeTab);
512                 break;
513         }
514     },
515 
516     setPanelTabStop : function (panel, elem)
517     {
518         var panelA11y = this.getPanelA11y(panel);
519         if (!panelA11y)
520             return;
521         var tabStop = this.getPanelTabStop(panel)
522         if (tabStop)
523         {
524             this.makeFocusable(tabStop, false);
525             if (["treeitem", "listitem", "option"].indexOf(tabStop.getAttribute("role")) != -1)
526                 tabStop.setAttribute("aria-selected", "false");
527         }
528         panelA11y.tabStop = elem;
529         if (elem)
530         {
531             panelA11y.reFocusId = null;
532             this.makeFocusable(elem, true);
533             if (["treeitem", "listitem", "option"].indexOf(elem.getAttribute("role")) != -1)
534                 elem.setAttribute("aria-selected", "true");
535         }
536     },
537 
538     findPanelTabStop : function(panel, className, last)
539     {
540         var candidates = panel.panelNode.getElementsByClassName(className);
541         candidates= Array.filter(candidates, function(e,i,a){return this.isVisibleByStyle(e) && isVisible(e);}, this);
542         if (candidates.length > 0)
543         {
544             var chosenRow = candidates[last ? candidates.length -1 : 0];
545             this.modifyPanelRow(panel, chosenRow, true)
546             this.setPanelTabStop(panel, chosenRow);
547         }
548         else
549             this.setPanelTabStop(panel, null);
550     },
551 
552     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
553     // Console Panel
554 
555     onLogRowCreated : function(panel, row)
556     {
557         var panelA11y = this.getPanelA11y(panel);
558         if (!panelA11y)
559             return;
560         if (hasClass(row, 'logRow-dir'))
561         {
562             row.setAttribute('role', 'listitem');
563             setClass(row, 'outerFocusRow');
564             var memberRows = row.getElementsByClassName('memberRow');
565             if (memberRows.length > 0)
566                 this.onMemberRowsAdded(panel, memberRows);
567         }
568         else if (hasClass(row, 'logRow-group') || hasClass(row, 'logRow-profile'))
569         {
570             row.setAttribute('role', 'presentation');
571             var focusRow = row.getElementsByClassName('logGroupLabel').item(0);
572             if (focusRow)
573             {
574                 this.setPanelTabStop(panel, focusRow);
575                 focusRow.setAttribute('aria-expanded', hasClass(row, 'opened') + "");
576                 if (!hasClass(row, 'logRow-profile'))
577                     this.insertHiddenText(panel, focusRow, 'group label: ');
578             }
579         }
580         else if (hasClass(row, 'logRow-errorMessage') || hasClass(row, 'logRow-warningMessage'))
581         {
582             setClass(row, 'outerFocusRow');
583             row.setAttribute('role', 'presentation');
584             var focusRow = row.getElementsByClassName('errorTitle').item(0);
585             if (focusRow)
586             {
587                 this.setPanelTabStop(panel, focusRow);
588                 focusRow.setAttribute('aria-expanded', hasClass(focusRow.parentNode, 'opened') + "");
589             }
590         }
591         else if (hasClass(row, 'logRow-stackTrace'))
592         {
593             setClass(row, 'outerFocusRow');
594             row.setAttribute('role', 'listitem');
595             var stackFrames = row.getElementsByClassName('focusRow');
596             Array.forEach(stackFrames, function(e,i,a){
597                 e.setAttribute('role', 'listitem');
598                 if ((panelA11y.lastIsDefault && i === stackFrames.length - 1) || (!panelA11y.lastIsDefault && i === 0))
599                     this.setPanelTabStop(panel, e);
600                 else
601                     this.makeFocusable(e, false);
602                 }, this);
603         }
604         else if (hasClass(row, 'logRow-spy'))
605         {
606             var focusRow = getChildByClass(row, 'spyHeadTable');
607             if (focusRow)
608                 this.makeFocusable(focusRow, true);
609         }
610         else
611         {
612             row.setAttribute('role', 'listitem');
613             setClass(row, 'focusRow');
614             setClass(row, 'outerFocusRow');
615             if (isVisible(row))
616                 this.setPanelTabStop(panel, row);
617         }
618     },
619 
620     modifyLogRow :function(panel, row, inTabOrder)
621     {
622         this.makeFocusable(row, inTabOrder);
623         var logRowType = this.getLogRowType(row);
624         if (logRowType)
625             this.insertHiddenText(panel, row, logRowType + ": ");
626         var arrayNode = getChildByClass(row, 'objectBox-array');
627         if (arrayNode)
628         {
629             arrayNode.setAttribute('role', 'group');
630             this.insertHiddenText(panel, row, "array" + ": ");
631         }
632         var focusObjects = this.getFocusObjects(row );
633         Array.forEach(focusObjects, function(e,i,a){
634             this.makeFocusable(e);
635             var prepend = "";
636             var append = " (" + this.getObjectType(e) + ") ";
637             if (e.textContent != "")
638                 e.setAttribute('aria-label', prepend + e.textContent + append);
639             if (arrayNode)
640                 e.setAttribute('role', 'listitem');
641             }, this);
642     },
643 
644     onNavigablePanelKeyPress : function(event)
645     {
646         var target = event.target;
647         var keyCode = event.keyCode || (event.type=='keypress' ? event.charCode : null);
648         if (!this.isTabWorthy(target) && !this.isFocusNoTabObject(target))
649             return;
650         else if (event.shiftKey || event.altKey)
651             return;
652         else if ([13, 32, 33, 34, 35, 36, 37, 38, 39, 40, 46].indexOf(keyCode) == -1)
653             return;//not interested in any other keys, than arrows, pg, home/end, del space & enter
654         var panel = Firebug.getElementPanel(target)
655         var panelA11y = this.getPanelA11y(panel);
656         if (!panelA11y)
657             return;
658         var newTarget = target
659         if (!this.isOuterFocusRow(target))
660         {
661             if (event.ctrlKey)
662             {
663                 newTarget = this.getAncestorRow(target);
664                 if (newTarget)
665                     newTarget = [33, 38].indexOf(keyCode) == -1 ? this.getLastFocusChild(newTarget) : this.getFirstFocusChild(newTarget)
666             }
667             else if (!this.isDirCell(target) || hasClass(target, 'netInfoTab') || hasClass(target, 'netCol'))
668                 newTarget = this.getAncestorRow(target, true);
669             if (!newTarget)
670                 newTarget = target;
671         }
672         switch (keyCode)
673         {
674             case 38://up
675             case 40://down
676             if (!this.isFocusNoTabObject(target))
677             {
678                 this.focusSiblingRow(panel, newTarget, keyCode == 38);
679                 cancelEvent(event);
680             }
681                 break;
682             case 37://left
683             case 39://right
684             var goLeft = keyCode == 37;
685             if (this.isDirCell(target))
686             {
687                 var row = getAncestorByClass(target, 'memberRow');
688                 var toggleElem = getChildByClass(row.cells[1], "memberLabel")
689                 if (!goLeft && hasClass(row, 'hasChildren'))
690                 {
691                     if (hasClass(row, 'opened'))
692                         this.focusSiblingRow(panel, target , false);
693                     else if (toggleElem)
694                     {
695                         if (hasClass(row, 'hasChildren'))
696                             target.setAttribute('aria-expanded', 'true');
697                         this.dispatchMouseEvent(toggleElem, 'click');
698                     }
699                 }
700                 else if (goLeft)
701                 {
702                     var level = parseInt(row.getAttribute("level"));
703                     if (hasClass(row, 'opened'))
704                     {
705                         if (hasClass(row, 'hasChildren'))
706                             target.setAttribute('aria-expanded', 'false');
707                         this.dispatchMouseEvent(toggleElem, 'click');
708                     }
709                     else if (level > 0)
710                     {
711                         var targetLevel = (level - 1) + "";
712                         var newRows = Array.filter(row.parentNode.rows, function(e,i,a){
713                             return e.rowIndex < row.rowIndex && e.getAttribute('level') == targetLevel;
714                             }, this);
715                         if (newRows.length)
716                             this.focus(newRows[newRows.length -1].cells[2].firstChild);
717                     }
718                 }
719                 cancelEvent(event);
720             }
721             else if (this.isOuterFocusRow(target, true))
722             {
723                 if (target.hasAttribute('aria-expanded'))
724                 {
725                     if (target.getAttribute('role') == 'row' || hasClass(target, 'spyHeadTable'))
726                     {
727                         if (goLeft && target.getAttribute('aria-expanded') == "true")
728                         {
729                             var toggleElem = hasClass(target, 'spyHeadTable') ? target.getElementsByClassName('spyTitleCol').item(0) : target;
730                             if (toggleElem)
731                                 this.dispatchMouseEvent(toggleElem, 'click');
732                         }
733                     }
734                     else if (target.getAttribute('aria-expanded') == (goLeft ? "true" : "false"))
735                         this.dispatchMouseEvent(target, hasClass(target, 'logGroupLabel') ? 'mousedown' : 'click');
736                 }
737                 if (goLeft)
738                 {
739                     //check if we're in an expanded section
740                     var inExpanded = false, groupClass, groupLabelClass, group, groupLabel;
741                     if (hasClass(target, 'objectBox-stackFrame'))
742                     {
743                         inExpanded = true;
744                         groupClass = "errorTrace";
745                         groupLabelClass = "errorTitle";
746                     }
747                     else if (getAncestorByClass(target, 'logGroupBody'))
748                     {
749                         inExpanded = true;
750                         groupClass = "logGroupBody";
751                         groupLabelClass = "logGroupLabel";
752                     }
753                     if (inExpanded)
754                     {
755                         group = getAncestorByClass(target, groupClass);
756                         if (group)
757                         {
758                             groupLabel = this.getPreviousByClass(target, groupLabelClass, false, panel.panelNode);
759                             if (groupLabel)
760                             {
761                                 this.modifyPanelRow(panel, groupLabel);
762                                 this.focus(groupLabel);
763                             }
764                         }
765                     }
766                 }
767                 else if (!goLeft)
768                 {
769 
770                     var focusItems = this.getFocusObjects(target);
771                     if (focusItems.length > 0)
772                         this.focus(event.ctrlKey ? focusItems[focusItems.length -1] : focusItems[0]);
773                 }
774             }
775             else if (this.isFocusObject(target))
776             {
777                 var parentRow = this.getAncestorRow(target, true);
778                 var focusObjects = this.getFocusObjects(parentRow);
779                 if (!event.ctrlKey)
780                 {
781                     var focusIndex = Array.indexOf(focusObjects, target);
782                     var newIndex = goLeft ? --focusIndex : ++focusIndex;
783                     if (goLeft && newIndex < 0)
784                         this.focus( parentRow);
785                     else
786                         this.focus(focusObjects[newIndex]);
787                 }
788                 else
789                     this.focus(goLeft ? parentRow : focusObjects[focusObjects.length -1]);
790                 cancelEvent(event);
791             }
792             break;
793         case 35://end
794         case 36://home
795             this.focusEdgeRow(panel, newTarget, keyCode == 36);
796             cancelEvent(event);
797             break;
798         case 33://pgup
799         case 34://pgdn
800             this.focusPageSiblingRow(panel, newTarget, keyCode == 33);
801             cancelEvent(event);
802             break;
803         case 13://enter
804             if (this.isFocusObject(target))
805             {
806                 this.dispatchMouseEvent(target, 'click');
807             }
808             else if(hasClass(target, 'watchEditBox'))
809             {
810                 this.dispatchMouseEvent(target, 'mousedown');
811                 cancelEvent(event);
812             }
813             else if (hasClass(target, 'breakpointRow'))
814             {
815                 var sourceLink = target.getElementsByClassName("objectLink-sourceLink").item(0);
816                 if (sourceLink)
817                     this.dispatchMouseEvent(sourceLink, 'click');
818             }
819             else if (target.hasAttribute('aria-expanded') && (target.getAttribute('role') == 'row' || target.getAttribute('role') == 'listitem'))
820             {
821                 var toggleElem = hasClass(target, 'spyHeadTable') ? target.getElementsByClassName('spyTitleCol').item(0) : target;
822                 if (toggleElem)
823                     this.dispatchMouseEvent(toggleElem, 'click');
824             }
825             break;
826         case 32://space
827         if (this.isFocusObject(target) && target.hasAttribute('role', 'checkbox'))
828         {
829             this.dispatchMouseEvent(target, 'click');
830             var objectBox = getAncestorByClass(target, 'hasBreakSwitch');
831             if (objectBox)
832                 target.setAttribute('aria-checked', hasClass(objectBox, 'breakForError') + "");
833         }
834         else if (hasClass(target, 'breakpointRow'))
835         {
836             var checkbox = target.getElementsByClassName('breakpointCheckbox').item(0);
837             if (checkbox)
838             {
839                 target.setAttribute('aria-checked', checkbox.checked ? "false" : "true");
840                 this.dispatchMouseEvent(checkbox, 'click');
841             }
842         }
843         break;
844         case 46://del
845             if (hasClass(target, 'breakpointRow'))
846             {
847                 var closeBtn = target.getElementsByClassName('closeButton').item(0);
848                 if (closeBtn)
849                 {
850                     var prevBreakpoint = getPreviousByClass(target, 'breakpointRow');
851                     if (prevBreakpoint)
852                         this.makeFocusable(prevBreakpoint, true);
853                     Firebug.chrome.window.document.commandDispatcher.rewindFocus();
854                     this.dispatchMouseEvent(closeBtn, 'click');
855                 }
856             }
857             break;
858         }
859     },
860 
861     focusPanelRow : function(panel, row)
862     {
863         var panelA11y = this.getPanelA11y(panel);
864         if (!panelA11y || !row)
865             return;
866         this.modifyPanelRow(panel, row, false);
867         if (panelA11y.cellIndex !== undefined && row.cells && row.cells[panelA11y.cellIndex]) //allows up / down navigation in columns, if columns are used in this panel
868         {
869             var cell = row.cells[panelA11y.cellIndex];
870             if (!hasClass(cell, "a11yFocus"))
871                 cell = getChildByClass(cell, 'a11yFocus');
872             this.focus(cell);
873         }
874         else if (hasClass(row, 'netInfoTabs')) // for Net Panel. Focus selected tab rather than the tablist
875         {
876             var tabs = row.getElementsByClassName('netInfoTab');
877             tabs = Array.filter(tabs, function(e,i,a){return e.hasAttribute('selected');})
878             this.focus(tabs.length > 0 ? tabs[0] : row);
879         }
880         else
881             this.focus(row);
882     },
883 
884     getRowIndex : function(rows, target)
885     {
886         return Array.indexOf(rows, target);
887     },
888 
889     getAncestorRow : function(elem, useSubRow)
890     {
891         return getAncestorByClass(elem, useSubRow ? 'focusRow' : 'outerFocusRow');
892     },
893 
894     onConsoleMouseDown : function(event)
895     {
896         var node = getAncestorByClass(event.target, 'focusRow');
897         if (node)
898             this.modifyPanelRow(Firebug.getElementPanel(node), node, false);
899         else
900         {
901             node = getAncestorByClass(event.target, 'memberRow');
902             if (!node)
903                 return;
904             var focusRow = node.getElementsByClassName('focusRow').item(0);
905             if (!focusRow)
906                 return
907             this.focusPanelRow(Firebug.getElementPanel(focusRow), focusRow);
908             node = getAncestorByClass(event.target, 'memberLabel')
909             if (!(node && hasClass(node, 'hasChildren')))
910                 cancelEvent(event);
911         }
912     },
913 
914     getValidRow : function(rows, index)
915     {
916         var min = 0; var max = rows.length -1;
917         if (index < min || index > max)
918             index = index < min ? 0 : max;
919         return rows[index];
920     },
921 
922     getFocusObjects : function(container)
923     {
924         var nodes = container.getElementsByClassName("a11yFocus")
925         return Array.filter(nodes, this.isVisibleByStyle, this);
926     },
927 
928     modifyConsoleRow : function(panel, row, inTabOrder)
929     {
930         if (this.isDirCell(row))
931             this.modifyMemberRow(panel, row, inTabOrder);
932         else if (this.isProfileRow(row))
933             this.modifyProfileRow(panel, row, inTabOrder);
934         else if (this.isOuterFocusRow(row, true))
935         {
936             if (hasClass(row, 'spyHeadTable') || hasClass(row, 'netInfoTabs'))
937                 this.modifyNetRow(panel, row, row.getAttribute('tabindex')=== '0');
938             else
939                 this.modifyLogRow(panel, row, row.getAttribute('tabindex')=== '0');
940         }
941         else return;
942     },
943 
944     modifyProfileRow : function(panel, row, inTabOrder)
945     {
946         var panelA11y = this.getPanelA11y(panel);
947         if (!panelA11y || !row)
948             return;
949             this.makeFocusable(row, inTabOrder);
950             var focusObjects = this.getFocusObjects(row);
951             Array.forEach(focusObjects, function(e,i,a) {
952                 this.makeFocusable(e);
953                 if (hasClass(e.parentNode, "profileCell"))
954                     e.setAttribute("role", "gridcell");
955             }, this);
956     },
957 
958     onConsoleSearchMatchFound : function(panel, text, matches)
959     {
960         var panelA11y = this.getPanelA11y(panel);
961         if (!panelA11y)
962             return;
963         var matchFeedback = "";
964         if (!matches || matches.length == 0)
965             matchFeedback = $STRF('a11y.updates.no matches found', [text]);
966         else
967             matchFeedback = $STRF('a11y.updates.match found in logrows', [text, matches.length]);
968         this.updateLiveElem(panel, matchFeedback, true); //should not use alert
969     },
970 
971     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
972     // HTML Panel
973 
974     onHTMLKeyPress: function(event)
975     {
976         var target = event.target;
977         var keyCode = event.keyCode || (event.type=='keypress' ? event.charCode : null);
978         if ([13, 32, KeyEvent.DOM_VK_F2].indexOf(keyCode) == -1)
979             return;
980         if (!hasClass(target, "nodeLabelBox"))
981             return;
982         var panel = Firebug.getElementPanel(target);
983         switch(keyCode)
984         {
985             case 13:
986             case 32:
987                 var isEnter = keyCode == 13;
988                 var nodeLabels = null;
989                 if (isEnter)
990                 {
991                     var nodeLabels = target.getElementsByClassName('nodeName');
992                     if (nodeLabels.length > 0)
993                     {
994                         Firebug.Editor.startEditing(nodeLabels[0]);
995                         cancelEvent(event);
996                     }
997                 }
998                 if (!isEnter || nodeLabels.length == 0)
999                 {
1000                     var nodeBox = getAncestorByClass(target, 'nodeBox');
1001                     if (nodeBox.repObject && panel.editNewAttribute)
1002                     {
1003                         panel.editNewAttribute(nodeBox.repObject)
1004                         cancelEvent(event);
1005                     }
1006                 }
1007                 break;
1008             case KeyEvent.DOM_VK_F2:
1009                 if (hasClass(target.parentNode.parentNode, 'textNodeBox'))
1010                 {
1011                     var textNode = getChildByClass(target, 'nodeText');
1012                     if (textNode)
1013                         Firebug.Editor.startEditing(textNode);
1014                 }
1015                 break;
1016         }
1017     },
1018 
1019     onHTMLFocus : function(event)
1020     {
1021         if (hasClass(event.target, 'nodeLabelBox'))
1022         {
1023             this.dispatchMouseEvent(event.target, 'mouseover');
1024             var nodeLabel = getAncestorByClass(event.target, 'nodeLabel');
1025             if (nodeLabel)
1026                 setClass(nodeLabel, 'focused');
1027             event.target.setAttribute("aria-selected", "true");
1028             cancelEvent(event);
1029         }
1030     },
1031 
1032     onHTMLBlur : function(event)
1033     {
1034         if (hasClass(event.target, 'nodeLabelBox'))
1035         {
1036             this.dispatchMouseEvent(event.target, 'mouseout');
1037             var nodeLabel = getAncestorByClass(event.target, 'nodeLabel');
1038             if (nodeLabel)
1039                 removeClass(nodeLabel, 'focused');
1040             event.target.setAttribute("aria-selected", "false");
1041             cancelEvent(event);
1042         }
1043     },
1044 
1045     onObjectBoxSelected: function(objectBox)
1046     {
1047         var panel = Firebug.getElementPanel(objectBox);
1048         var panelA11y = this.getPanelA11y(panel);
1049         if (!panelA11y)
1050             return;
1051         var label = objectBox.firstChild.getElementsByClassName('nodeLabelBox').item(0);
1052         if (label) {
1053             this.makeFocusable(label, true);
1054             if (this.panelHasFocus(panel))
1055                 this.focus(label);
1056         }
1057     },
1058 
1059     onObjectBoxUnselected: function(objectBox)
1060     {
1061         if (!this.isEnabled() ||  !objectBox)
1062             return;
1063         var label = objectBox.firstChild.getElementsByClassName('nodeLabelBox').item(0);
1064         if (label) {
1065             this.makeUnfocusable(label, true);
1066         }
1067     },
1068 
1069     onHTMLSearchMatchFound: function(panel, match)
1070     {
1071         var panelA11y = this.getPanelA11y(panel);
1072         if (!panelA11y)
1073             return;
1074         var node = match.node;
1075         var elem;
1076         var matchFeedback = ""; 
1077         switch (node.nodeType)
1078         {
1079             case 1: //element
1080                 elem = node;
1081                 matchFeedback += $STRF("a11y.updates.match found in element", [match.match[0], elem.nodeName, getElementTreeXPath(elem)]);
1082                 break;
1083             case 2: //attribute
1084                 elem = node.ownerElement;
1085                 matchFeedback += $STRF("a11y.updates.match found in attribute", 
1086                     [match.match[0], node.nodeName, node.nodeValue, elem.nodeName, getElementTreeXPath(elem)]);   
1087                 break;
1088                 
1089             case 3: //text content
1090                 elem = node.parentNode;
1091                 matchFeedback += $STRF("a11y.updates.match found in text content", [match.match[0], match.match.input]); 
1092                 break;
1093         }
1094         this.updateLiveElem(panel, matchFeedback, true); //should not use alert
1095     },
1096 
1097     onHTMLSearchNoMatchFound: function(panel, text)
1098     {
1099         this.updateLiveElem(panel, $STRF('a11y.updates.no matches found', [text]), true); //should not use alert
1100     },
1101 
1102     moveToSearchMatch: function()
1103     {
1104         if (!this.isEnabled())
1105             return;
1106         var panel = Firebug.chrome.getSelectedPanel();
1107         var panelA11y = this.getPanelA11y(panel);
1108         if (!panelA11y || !panel.searchable )
1109             return;
1110         var popup = Firebug.chrome.$('fbSearchOptionsPopup');
1111         if (popup)
1112             popup.hidePopup();
1113         switch(panelA11y.type)
1114         {
1115             case 'html':
1116                 var match = panel.lastSearch.lastMatch;
1117                 if (!match)
1118                     return;
1119                 var nodeBox = panel.lastSearch.openToNode(match.node, match.isValue);
1120                 if (!nodeBox)
1121                     return;
1122                 nodeBox = getAncestorByClass(nodeBox, 'nodeBox');
1123                 panel.select(nodeBox.repObject, true);
1124                 break;
1125             case 'stylesheet':
1126                 if (panel.currentSearch && panel.currentSearch.currentNode)
1127                 {
1128                     var focusRow = getAncestorByClass(panel.currentSearch.currentNode, 'focusRow');
1129                     if (focusRow)
1130                     {
1131                         this.focusPanelRow(panel, focusRow);
1132                     }
1133                 }
1134                 break
1135             case 'script' :
1136                 if (panel.currentSearch && panel.selectedSourceBox)
1137                 {
1138                     var box = panel.selectedSourceBox;
1139                     var lineNo = panel.currentSearch.mark;
1140                     box.a11yCaretLine = lineNo + 1;
1141                     box.a11yCaretOffset = 0;
1142                     panel.scrollToLine(box.repObject.href, lineNo, panel.jumpHighlightFactory(lineNo+1, panel.context));
1143                     var viewport = box.getElementsByClassName('sourceViewport').item(0);
1144                     if(viewport)
1145                     {
1146                         this.focus(viewport);
1147                         this.insertCaretIntoLine(panel, box);
1148                     }
1149                 }
1150                 break;
1151             case 'dom':
1152                 if (panel.currentSearch && panel.currentSearch.currentNode)
1153                 {
1154                     var focusRow = panel.currentSearch.currentNode.getElementsByClassName('focusRow').item(0);
1155                     if (focusRow)
1156                     {
1157                         this.focusPanelRow(panel, focusRow);
1158                     }
1159                 }
1160                 break;
1161             case 'net':
1162                 if (panel.currentSearch && panel.currentSearch.currentNode)
1163                 {
1164                     var focusRow = getAncestorByClass(panel.currentSearch.currentNode, 'focusRow');
1165                     if (focusRow)
1166                     {
1167                         this.focusPanelRow(panel, focusRow);
1168                     }
1169                 }
1170                 break;
1171         }
1172     },
1173 
1174     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1175     // CSS Panel
1176 
1177     onCSSKeyPress : function(event)
1178     {
1179         var target = event.target;
1180         var keyCode = event.keyCode || (event.type=='keypress' ? event.charCode : null);
1181         if (!this.isFocusRow(target))
1182             return;
1183         else if (event.altKey)
1184             return;
1185         else if ([13, 32, 33, 34, 35, 36, 38,  40].indexOf(keyCode) == -1)
1186             return;//not interested in any other keys, than arrows, pg, home/end, space & enter
1187         var panel = Firebug.getElementPanel(target)
1188         var panelA11y = this.getPanelA11y(panel);
1189         if (!panelA11y)
1190             return;
1191         switch (keyCode)
1192         {
1193             case 38://up
1194             case 40://down
1195                 var goUp = keyCode == 38;
1196                 if (event.ctrlKey)
1197                 {
1198                     if (event.shiftKey)
1199                     {
1200                         var node = this[goUp ? 'getPreviousByClass' : 'getNextByClass'](target, 'cssInheritHeader', panel.panelNode);
1201                         if (node)
1202                         {
1203                             this.focusPanelRow(panel, node);
1204                         }
1205                         else if (goUp)
1206                            this.focusEdgeCSSRow(panel, target, true);
1207                     }
1208                     else
1209                         this.focusSiblingHeadRow(panel, target, goUp);
1210                 }
1211                 else
1212                     this.focusSiblingCSSRow(panel, target, goUp);
1213                 break;
1214             case 35://end
1215             case 36://home
1216                 if (event.ctrlKey)
1217                     this.focusEdgeHeadRow(panel, target, keyCode == 36);
1218                 else
1219                     this.focusEdgeCSSRow(panel, target, keyCode == 36);
1220                 break;
1221             case 33://pgup
1222             case 34://pgdn
1223                 if (event.ctrlKey)
1224                     this.focusPageSiblingHeadRow(panel, target, keyCode == 33);
1225                 else
1226                     this.focusPageSiblingCSSRow(panel, target, keyCode == 33);
1227                 break;
1228             case 13://enter
1229                 if (hasClass(target, 'cssProp'))
1230                 {
1231                     var node = getChildByClass(target, 'cssPropName');
1232                     if (node)
1233                         Firebug.Editor.startEditing(node);
1234                     cancelEvent(event);
1235                 }
1236                 else if (hasClass(target, 'cssHead'))
1237                 {
1238                     var node = getChildByClass(target, 'cssSelector');
1239                     if (node && hasClass(node, 'editable'))
1240                         Firebug.Editor.startEditing(node);
1241                     cancelEvent(event);
1242                 }
1243                 else if (hasClass(target, 'importRule'))
1244                 {
1245                     var node = getChildByClass(target, 'objectLink');
1246                     if (node)
1247                         this.dispatchMouseEvent(node, 'click');
1248                 }
1249                 break;
1250             case 32://space
1251                 if (hasClass(target, 'cssProp'))
1252                 {
1253                     //our focus is about to be wiped out, we'll try to get it back after
1254                     panelA11y.reFocusId = getElementXPath(target);
1255                     panel.disablePropertyRow(target);
1256                     if (panel.name == "stylesheet")
1257                         target.setAttribute('aria-checked', !hasClass(target, 'disabledStyle'));
1258                     cancelEvent(event);
1259                 }
1260                 break;
1261         }
1262         if (!event.shiftKey)
1263             event.preventDefault();
1264     },
1265 
1266     onCSSMouseDown : function(event)
1267     {
1268         var row = getAncestorByClass(event.target, 'focusRow');
1269         if (row)
1270             this.modifyPanelRow(Firebug.getElementPanel(row), row, false);
1271     },
1272 
1273     focusSiblingCSSRow : function(panel, target, goUp)
1274     {
1275         var newRow = this[goUp ? 'getPreviousByClass' : 'getNextByClass'](target, 'focusRow', panel.panelNode)
1276         if (!newRow)
1277             return;
1278         this.focusPanelRow(panel, newRow, false);
1279     },
1280 
1281     focusPageSiblingCSSRow : function(panel, target, goUp)
1282     {
1283         var rows = this.getFocusRows(panel);
1284         var index = this.getRowIndex(rows, target);
1285         var newRow = this.getValidRow(rows, goUp ? index - 10 : index + 10);
1286         this.focusPanelRow(panel, newRow, false);
1287     },
1288 
1289     focusEdgeCSSRow : function(panel, target, goUp)
1290     {
1291         var rows = this.getFocusRows(panel);
1292         var newRow = this.getValidRow(rows, goUp ? 0 : rows.length -1);
1293         this.focusPanelRow(panel, newRow, false);
1294     },
1295 
1296     getHeadRowsAndIndex: function(panel, elem)
1297     {
1298         var rows = this.getFocusRows(panel);
1299         var headRow = hasClass(elem, 'cssHead') ? elem : getPreviousByClass(elem, 'cssHead');
1300         var headRows = Array.filter(rows, function(e,i,a){return hasClass(e, 'cssHead')});
1301         var index = Array.indexOf(headRows, headRow);
1302         if (index == -1)
1303             index = 0;
1304         return [headRows, index]
1305     },
1306 
1307     focusSiblingHeadRow : function(panel, elem, goUp)
1308     {
1309         var rowInfo = this.getHeadRowsAndIndex(panel, elem);
1310         var newRow = this.getValidRow(rowInfo[0], goUp ? rowInfo[1] - 1 : rowInfo[1] + 1);
1311         this.focusPanelRow(panel, newRow, false);
1312     },
1313 
1314     focusPageSiblingHeadRow : function(panel, elem, goUp)
1315     {
1316         var rowInfo = this.getHeadRowsAndIndex(panel, elem);
1317         var newRow = this.getValidRow(rowInfo[0], goUp ? rowInfo[1] - 10 : rowInfo[1] + 10);
1318         this.focusPanelRow(panel, newRow, false);
1319     },
1320 
1321     focusEdgeHeadRow : function(panel, elem, goUp)
1322     {
1323         var rowInfo = this.getHeadRowsAndIndex(panel, elem);
1324         var newRow = this.getValidRow(rowInfo[0], goUp ? 0 : rowInfo[0].length - 1);
1325         this.focusPanelRow(panel, newRow, false);
1326     },
1327 
1328     onBeforeCSSRulesAdded : function(panel)
1329     {
1330         // Panel content is about to be recreated, possibly wiping out focus.
1331         // Use the focused element's xpath to remember which rule had focus so that it can be refocused when the panel content is drawn again
1332         var panelA11y = this.getPanelA11y(panel);
1333         if (!panelA11y || !this.panelHasFocus(panel))
1334             return;
1335         if (panelA11y.tabStop && hasClass(panelA11y.tabStop, 'focusRow'))
1336             panelA11y.reFocusId = getElementXPath(panelA11y.tabStop);
1337     },
1338 
1339     onCSSRulesAdded : function(panel, rootNode)
1340     {
1341         var panelA11y = this.getPanelA11y(panel);
1342         if (!panelA11y)
1343             return;
1344         var row;
1345         if (panelA11y.reFocusId)
1346         {   //we need to put focus back to where it was before it was wiped out
1347             var reFocusRows = getElementsByXPath(rootNode.ownerDocument, panelA11y.reFocusId);
1348             panelA11y.reFocusId = null;
1349             if (reFocusRows.length > 0)
1350             {
1351                 row = reFocusRows[0];
1352                 this.modifyPanelRow(panel, row, true);
1353                 this.focus(row, true);
1354                 this.setPanelTabStop(panel, row);
1355                 return;
1356             }
1357         }
1358         //no refocus needed, just make first rule the panel's tab stop
1359         row = rootNode.getElementsByClassName('focusRow').item(0);
1360         this.modifyPanelRow(panel, row, true);
1361         return;
1362     },
1363     //applies a11y changes (keyboard and screen reader related) to an individual row
1364     //To improve performance, this only happens when absolutely necessary, e.g. when the user navigates to the row in question
1365 
1366     modifyCSSRow : function(panel, row, inTabOrder)
1367     {
1368         if (!panel || !row)
1369             return;
1370         var rule = getAncestorByClass(row, "cssRule");
1371         if (inTabOrder)
1372             this.setPanelTabStop(panel, row);
1373         else
1374             this.makeFocusable(row);
1375         if (rule && !hasClass(rule, 'a11yModified'))
1376         {
1377             var listBox = rule.getElementsByClassName('cssPropertyListBox').item(0);
1378             var selector = rule.getElementsByClassName('cssSelector').item(0);
1379             if (listBox && selector)
1380                 listBox.setAttribute('aria-label', $STRF("a11y.labels.declarations for selector", [selector.textContent]));
1381             setClass(rule, 'a11yModified')
1382         }
1383         if (hasClass(row, 'cssHead'))
1384         {
1385             if (panel.name == "css")
1386             {
1387                 var sourceLink = rule.parentNode.lastChild;
1388                 if (sourceLink && hasClass(sourceLink, "objectLink"))
1389                     row.setAttribute('aria-label', row.textContent + " " + $STRF('a11y.labels.defined in file', [sourceLink.textContent]));
1390             }
1391         }
1392         else if (hasClass(row, 'cssProp'))
1393         {
1394             row.setAttribute('aria-checked', !hasClass(row, 'disabledStyle'));
1395             if (hasClass(row, 'cssOverridden'))
1396                 row.setAttribute('aria-label', $STR('aria.labels.overridden') + " " + row.textContent);
1397         }
1398         return;
1399     },
1400 
1401     onCSSPanelContextMenu : function(event)
1402     {
1403         var panelA11y = this.getPanelA11y(panel);
1404         if (!panelA11y)
1405             return;
1406         if (event.button == 0) //the event was created by keyboard, not right mouse click
1407         {
1408             var panel = Firebug.getElementPanel(event.target);
1409             if (panel && hasClass(event.target, 'focusRow'))
1410             {
1411                 var node = event.target;
1412                 if (panel.name == "css")
1413                 {
1414                     if (hasClass(event.target, 'cssHead'))
1415                         node = event.target.parentNode.getElementsByClassName('objectLink').item(0);
1416                     else if (hasClass(event.target, 'cssInheritHeader'))
1417                         node = event.target.getElementsByClassName('objectLink').item(0);
1418                     if (!node || hasClass(node, 'collapsed'))
1419                         node = event.target;
1420                 }
1421                 //these context menu options are likely to destroy current focus
1422                 panelA11y.reFocusId = getElementXPath(event.target);
1423                 document.popupNode = node;
1424                 Firebug.chrome.$('fbContextMenu').openPopup(node, 'overlap', 0,0,true);
1425                 cancelEvent(event); //no need for default handlers anymore
1426             }
1427         }
1428     },
1429 
1430     onCSSSearchMatchFound : function(panel, text, matchRow)
1431     {
1432         var panelA11y = this.getPanelA11y(panel);
1433         if (!panelA11y || !text)
1434             return;
1435         if (!matchRow)
1436         {
1437             this.updateLiveElem(panel, $STRF('a11y.updates.no matches found', [text]), true); //should not use alert
1438             return;
1439         }
1440         var matchFeedback = "";
1441         var matchType = '';
1442         var selector;
1443         if (hasClass(matchRow, 'cssSelector'))
1444             matchFeedback = " " + $STRF('a11y.updates.match found in selector', [text, matchRow.textContent]);
1445         else
1446         {
1447             selector = getPreviousByClass(matchRow, 'cssSelector');
1448             selector = selector ? selector.textContent : "";
1449             if (hasClass(matchRow, 'cssPropName') || hasClass(matchRow, 'cssPropValue'))
1450             {
1451                 var propRow = getAncestorByClass(matchRow, 'cssProp');
1452                 if (propRow)
1453                     matchFeedback = $STRF('a11y.updates.match found in style declaration', [text, propRow.textContent, selector]);
1454             }
1455         }
1456         this.updateLiveElem(panel, matchFeedback, true); // should not use alert
1457     },
1458 
1459     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1460     // Layout Panel
1461 
1462     onLayoutBoxCreated : function(panel, node, detailsObj)
1463     {
1464         var panelA11y = this.getPanelA11y(panel);
1465         if (!panelA11y)
1466             return;
1467         var focusGroups = node.getElementsByClassName('focusGroup');
1468         Array.forEach(focusGroups, function(e,i,a){
1469             if (hasClass(e, 'positionLayoutBox'))
1470                 this.makeFocusable(e, true);
1471             else
1472                 this.makeFocusable(e, false);
1473             e.setAttribute('role', 'group');
1474             e.setAttribute('aria-label', this.getLayoutBoxLabel(e, detailsObj));
1475             e.setAttribute('aria-setsize', a.length);
1476             e.setAttribute('aria-posinset', i + 1);
1477         }, this);
1478     },
1479 
1480     getLayoutBoxLabel : function(elem, detailsObj )
1481     {
1482         var className = elem.className.match(/\b(\w+)LayoutBox\b/);
1483         if (!className)
1484             return "";
1485         var styleName = className[1];
1486         var output = "";
1487         switch(styleName)
1488         {
1489             case "position":
1490                 output += hasClass(elem, "blankEdge") ? '' : $STR("a11y.layout.position");
1491                 styleName = "outer";
1492                 break;
1493             case "margin":
1494                 output += $STR("a11y.layout.margin");
1495                 break;
1496             case "border":
1497                 output += $STR("a11y.layout.border");
1498                 break;
1499             case "padding":
1500                 output += $STR("a11y.layout.padding");
1501                 break;
1502             case "content":
1503                 output += $STR("a11y.layout.size");
1504                 break;
1505         }
1506         output += ": ";
1507         var valNames = [];
1508         var vals = {};
1509         switch (styleName)
1510         {
1511             case "outer":        
1512                 valNames = ['top', 'left', "position", "z-index"];
1513                 vals.top = detailsObj[styleName + 'Top'];
1514                 vals.left = detailsObj[styleName + 'Left'];
1515                 vals.position = detailsObj.position;
1516                 vals["z-index"] = detailsObj.zIndex;
1517                 break;
1518             case "content":
1519                 valNames = ['width', 'height']
1520                 vals.width = detailsObj['width'];
1521                 vals.height = detailsObj['height'];
1522                 break;
1523             default:
1524                 valNames = ['top', 'right', 'bottom', 'left'];
1525                 vals.top = detailsObj[styleName + 'Top'];
1526                 vals.right = detailsObj[styleName + 'Right'];
1527                 vals.bottom = detailsObj[styleName + 'Bottom'];
1528                 vals.left = detailsObj[styleName + 'Left'];
1529                 break;
1530         }
1531         
1532         for (var i = 0 ; i < valNames.length; i++)
1533         {
1534             output += $STR("a11y.layout." + valNames[i]) + " = " + vals[valNames[i]];
1535             output += i == valNames.length -1 ? "" : ", ";
1536         }
1537         return output;
1538     },
1539 
1540     onLayoutKeyPress : function(event)
1541     {
1542         var target = event.target;
1543         var keyCode = event.keyCode || (event.type=='keypress' ? event.charCode : null);
1544         if ([13, 37, 38, 39, 40].indexOf(keyCode) == -1)
1545             return;
1546         if (!hasClass(target, 'focusGroup'))
1547             return;
1548         var panel = Firebug.getElementPanel(target);
1549         switch(keyCode)
1550         {
1551             case 37:
1552             case 38:
1553             case 39:
1554             case 40:
1555                 var node, goLeft = keyCode == 37 || keyCode == 38;
1556                 if (goLeft)
1557                     node = getAncestorByClass(target.parentNode, 'focusGroup');
1558                 else
1559                     node = getChildByClass(target, 'focusGroup');
1560                 if (node)
1561                     this.focus(node);
1562                 break;
1563             case 13:
1564                 var editable = target.getElementsByClassName('editable').item(0);
1565                 if (editable)
1566                     Firebug.Editor.startEditing(editable);
1567                 cancelEvent(event);
1568                 break;
1569         }
1570     },
1571 
1572     onLayoutFocus : function(event)
1573     {
1574         if (hasClass(event.target, 'focusGroup'))
1575         {
1576             this.dispatchMouseEvent(event.target, 'mouseover');
1577             this.setPanelTabStop(Firebug.getElementPanel(event.target), event.target);
1578         }
1579     },
1580 
1581     onLayoutBlur : function(event)
1582     {
1583         if (hasClass(event.target, 'focusGroup'))
1584             this.dispatchMouseEvent(event.target, 'mouseout');
1585     },
1586 
1587     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1588     // Inline Editing
1589     onInlineEditorShow : function(panel, editor)
1590     {
1591         var panelA11y = this.getPanelA11y(panel);
1592         if (!panelA11y)
1593             return;
1594         //recreate the input element rather than reusing the old one, otherwise AT won't pick it up
1595         editor.input.onkeypress = editor.input.oninput = editor.input.onoverflow = null;
1596         editor.inputTag.replace({}, editor.box.childNodes[1].firstChild, editor);
1597         editor.input = editor.box.childNodes[1].firstChild.firstChild;
1598     },
1599 
1600     onBeginEditing : function(panel, editor, target, value)
1601     {
1602         var panelA11y = this.getPanelA11y(panel);
1603         if (!panelA11y)
1604             return;
1605         switch (panelA11y.type)
1606         {
1607             case 'html':
1608                 var tagName= nodeName = null;
1609                 var setSize = posInSet = 0; var setElems;
1610                 var label = $STR("a11y.labels.inline editor") + ": ";
1611                 if (hasClass(target, 'nodeName') || hasClass(target, 'nodeValue'))
1612                 {
1613                     var isName = hasClass(target, 'nodeName');
1614                     setElems = target.parentNode.parentNode.getElementsByClassName(isName ? 'nodeName' : 'nodeValue');
1615                     setSize = (setElems.length * 2);
1616                     posInSet = ((Array.indexOf(setElems, target) + 1) * 2) - (isName ? 1 : 0);
1617                     editor.input.setAttribute('role', 'listitem');
1618                     editor.input.setAttribute('aria-setsize', setSize);
1619                     editor.input.setAttribute('aria-posinset', posInSet);
1620                     nodeTag = getPreviousByClass(target, 'nodeTag');
1621                     if (!isName)
1622                     {
1623                         nodeName = getPreviousByClass(target, 'nodeName');
1624                         label += $STRF('a11y.labels.value for attribute in element', [nodeName.textContent, nodeTag.textContent]);
1625                     }
1626                     else
1627                         label += $STRF("a11y.label.attribute for element", [nodeTag.textContent]);
1628                 }
1629                 else if (hasClass(target, 'nodeText'))
1630                 {
1631                     nodeTag = getPreviousByClass(target, 'nodeTag');
1632                     label += $STRF("a11y.labels.text contents for element", [nodeTag.textContent]);
1633                 }
1634                 editor.input.setAttribute('aria-label', label);
1635                 break;
1636             case 'css':
1637             case 'stylesheet':
1638                 var selector = getPreviousByClass(target, 'cssSelector');
1639                 selector = selector ? selector.textContent : "";
1640                 var label = $STR("a11y.labels.inline editor") + ": ";
1641                 if (hasClass(target, 'cssPropName'))
1642                     label += $STRF('a11y.labels.property for selector', [selector]);
1643                 else if (hasClass(target, 'cssPropValue'))
1644                 {
1645                     var propName = getPreviousByClass(target, 'cssPropName');
1646                     propName = propName ? propName.textContent : "";
1647                     label += $STRF('a11y.labels.value property in selector', [propName, selector]);
1648                 }
1649                 else if (hasClass(target, 'cssSelector'))
1650                     label += $STR('a11y.labels.css selector');
1651                 editor.input.setAttribute('aria-label', label);
1652                 editor.setAttribute('aria-autocomplete', 'inline');
1653                 break;
1654             case 'layout':
1655                 editor.input.setAttribute('aria-label', target.getAttribute('aria-label'));
1656                 break;
1657             case 'dom':
1658             case 'domSide':
1659                 if (target.cells && target.cells[1])
1660                     editor.input.setAttribute('aria-label', target.cells[1].textContent);
1661                 break;
1662         }
1663     },
1664 
1665     onInlineEditorClose  : function(panel, target, removeGroup)
1666     {
1667         var panelA11y = this.getPanelA11y(panel);
1668         if (!panelA11y)
1669             return;
1670         switch (panelA11y.type)
1671         {
1672             case 'layout':
1673                 var box = getAncestorByClass(target, 'focusGroup')
1674                 if (box)
1675                     this.focus(box, true);
1676                 break;
1677             case 'css':
1678             case 'stylesheet':
1679                 var node = target.parentNode;
1680                 if (removeGroup)
1681                     node = this.getPreviousByClass(node, 'focusRow', panel.panelNode);
1682                 if (node)
1683                 {
1684                     this.focusPanelRow(panel, node, true);
1685                 }
1686                 break;
1687             case 'html':
1688                 var box = getAncestorByClass(target, 'nodeBox')
1689                 if (box)
1690                     panel.select(box.repObject, true);
1691                 break;
1692             case 'watches':
1693                 var node = target.getElementsByClassName('watchEditBox').item(0);
1694                 if (node)
1695                     this.focus(node, true);
1696                 break;
1697             case 'script':
1698                 panel.selectedSourceBox.focus();
1699                 break;
1700         }
1701     },
1702 
1703     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1704     // Script Panel
1705 
1706     onStop : function(context, frame, type,rv)
1707     {
1708         if (!context)
1709             return;
1710         var panel = context.getPanel('script');
1711         var panelA11y = this.getPanelA11y(panel);
1712         if (!panelA11y)
1713             return;
1714         var fileName =  frame.script.fileName.split("/");
1715         fileName = fileName.pop();
1716         var alertString = $STRF("a11y.updates.script_suspended_on_line_in_file",[frame.line, frame.functionName, fileName]);
1717         this.updateLiveElem(panel, alertString, true);
1718         this.onShowSourceLink(panel, frame.line);
1719     },
1720 
1721     onShowSourceLink : function (panel, lineNo)
1722     {
1723         if (!this.isEnabled())
1724             return;
1725         var box = panel.selectedSourceBox;
1726         var viewport = box.getElementsByClassName('sourceViewport').item(0);
1727         box.a11yCaretLine = lineNo;
1728         if (viewport && this.panelHasFocus(panel))
1729         {
1730