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