1 /* See license.txt for terms of usage */ 2 3 FBL.ns(function() { with (FBL) { 4 5 // ************************************************************************************************ 6 // Constants 7 8 const Cc = Components.classes; 9 const Ci = Components.interfaces; 10 const nsIURI = Ci.nsIURI; 11 const nsIDOMCSSStyleRule = Ci.nsIDOMCSSStyleRule; 12 const nsIInterfaceRequestor = Ci.nsIInterfaceRequestor; 13 const nsISelectionDisplay = Ci.nsISelectionDisplay; 14 const nsISelectionController = Ci.nsISelectionController; 15 16 // See: http://mxr.mozilla.org/mozilla1.9.2/source/content/events/public/nsIEventStateManager.h#153 17 const STATE_ACTIVE = 0x01; 18 const STATE_FOCUS = 0x02; 19 const STATE_HOVER = 0x04; 20 21 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 22 23 const domUtils = CCSV("@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); 24 25 var CSSDomplateBase = { 26 isEditable: function(rule) 27 { 28 return !rule.isSystemSheet; 29 }, 30 isSelectorEditable: function(rule) 31 { 32 return rule.isSelectorEditable && this.isEditable(rule); 33 } 34 }; 35 36 var CSSPropTag = domplate(CSSDomplateBase, { 37 tag: DIV({class: "cssProp focusRow", $disabledStyle: "$prop.disabled", 38 $editGroup: "$rule|isEditable", 39 $cssOverridden: "$prop.overridden", role : "option"}, 40 SPAN({class: "cssPropName", $editable: "$rule|isEditable"}, "$prop.name"), 41 SPAN({class: "cssColon"}, ":"), 42 SPAN({class: "cssPropValue", $editable: "$rule|isEditable"}, "$prop.value$prop.important"), 43 SPAN({class: "cssSemi"}, ";") 44 ) 45 }); 46 47 var CSSRuleTag = 48 TAG("$rule.tag", {rule: "$rule"}); 49 50 var CSSImportRuleTag = domplate({ 51 tag: DIV({class: "cssRule insertInto focusRow importRule", _repObject: "$rule.rule"}, 52 "@import "", 53 A({class: "objectLink", _repObject: "$rule.rule.styleSheet"}, "$rule.rule.href"), 54 "";" 55 ) 56 }); 57 58 var CSSStyleRuleTag = domplate(CSSDomplateBase, { 59 tag: DIV({class: "cssRule insertInto", 60 $cssEditableRule: "$rule|isEditable", 61 $editGroup: "$rule|isSelectorEditable", 62 _repObject: "$rule.rule", 63 "ruleId": "$rule.id", role : 'presentation'}, 64 DIV({class: "cssHead focusRow", role : 'listitem'}, 65 SPAN({class: "cssSelector", $editable: "$rule|isSelectorEditable"}, "$rule.selector"), " {" 66 ), 67 DIV({role : 'group'}, 68 DIV({class : "cssPropertyListBox", role : 'listbox'}, 69 FOR("prop", "$rule.props", 70 TAG(CSSPropTag.tag, {rule: "$rule", prop: "$prop"}) 71 ) 72 ) 73 ), 74 DIV({class: "editable insertBefore", role:"presentation"}, "}") 75 ) 76 }); 77 78 const reSplitCSS = /(url\("?[^"\)]+?"?\))|(rgb\(.*?\))|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,2})?)|([^,\s]+)|"(.*?)"/; 79 80 const reURL = /url\("?([^"\)]+)?"?\)/; 81 82 const reRepeat = /no-repeat|repeat-x|repeat-y|repeat/; 83 84 const sothinkInstalled = !!$("swfcatcherKey_sidebar"); 85 const styleGroups = 86 { 87 text: [ 88 "font-family", 89 "font-size", 90 "font-weight", 91 "font-style", 92 "color", 93 "text-transform", 94 "text-decoration", 95 "letter-spacing", 96 "word-spacing", 97 "line-height", 98 "text-align", 99 "vertical-align", 100 "direction", 101 "column-count", 102 "column-gap", 103 "column-width" 104 ], 105 106 background: [ 107 "background-color", 108 "background-image", 109 "background-repeat", 110 "background-position", 111 "background-attachment", 112 "opacity" 113 ], 114 115 box: [ 116 "width", 117 "height", 118 "top", 119 "right", 120 "bottom", 121 "left", 122 "margin-top", 123 "margin-right", 124 "margin-bottom", 125 "margin-left", 126 "padding-top", 127 "padding-right", 128 "padding-bottom", 129 "padding-left", 130 "border-top-width", 131 "border-right-width", 132 "border-bottom-width", 133 "border-left-width", 134 "border-top-color", 135 "border-right-color", 136 "border-bottom-color", 137 "border-left-color", 138 "border-top-style", 139 "border-right-style", 140 "border-bottom-style", 141 "border-left-style", 142 "-moz-border-top-radius", 143 "-moz-border-right-radius", 144 "-moz-border-bottom-radius", 145 "-moz-border-left-radius", 146 "outline-top-width", 147 "outline-right-width", 148 "outline-bottom-width", 149 "outline-left-width", 150 "outline-top-color", 151 "outline-right-color", 152 "outline-bottom-color", 153 "outline-left-color", 154 "outline-top-style", 155 "outline-right-style", 156 "outline-bottom-style", 157 "outline-left-style" 158 ], 159 160 layout: [ 161 "position", 162 "display", 163 "visibility", 164 "z-index", 165 "overflow-x", // http://www.w3.org/TR/2002/WD-css3-box-20021024/#overflow 166 "overflow-y", 167 "overflow-clip", 168 "white-space", 169 "clip", 170 "float", 171 "clear", 172 "-moz-box-sizing" 173 ], 174 175 other: [ 176 "cursor", 177 "list-style-image", 178 "list-style-position", 179 "list-style-type", 180 "marker-offset", 181 "user-focus", 182 "user-select", 183 "user-modify", 184 "user-input" 185 ] 186 }; 187 188 Firebug.CSSModule = extend(Firebug.Module, 189 { 190 freeEdit: function(styleSheet, value) 191 { 192 if (!styleSheet.editStyleSheet) 193 { 194 var ownerNode = getStyleSheetOwnerNode(styleSheet); 195 styleSheet.disabled = true; 196 197 var url = CCSV("@mozilla.org/network/standard-url;1", Components.interfaces.nsIURL); 198 url.spec = styleSheet.href; 199 200 var editStyleSheet = ownerNode.ownerDocument.createElementNS( 201 "http://www.w3.org/1999/xhtml", 202 "style"); 203 unwrapObject(editStyleSheet).firebugIgnore = true; 204 editStyleSheet.setAttribute("type", "text/css"); 205 editStyleSheet.setAttributeNS( 206 "http://www.w3.org/XML/1998/namespace", 207 "base", 208 url.directory); 209 if (ownerNode.hasAttribute("media")) 210 { 211 editStyleSheet.setAttribute("media", ownerNode.getAttribute("media")); 212 } 213 214 // Insert the edited stylesheet directly after the old one to ensure the styles 215 // cascade properly. 216 ownerNode.parentNode.insertBefore(editStyleSheet, ownerNode.nextSibling); 217 218 styleSheet.editStyleSheet = editStyleSheet; 219 } 220 221 styleSheet.editStyleSheet.innerHTML = value; 222 if (FBTrace.DBG_CSS) 223 FBTrace.sysout("css.saveEdit styleSheet.href:"+styleSheet.href+" got innerHTML:"+value+"\n"); 224 225 dispatch(this.fbListener, "onCSSFreeEdit", [styleSheet, value]); 226 }, 227 228 insertRule: function(styleSheet, cssText, ruleIndex) 229 { 230 if (FBTrace.DBG_CSS) FBTrace.sysout("Insert: " + ruleIndex + " " + cssText); 231 var insertIndex = styleSheet.insertRule(cssText, ruleIndex); 232 233 dispatch(this.fbListeners, "onCSSInsertRule", [styleSheet, cssText, ruleIndex]); 234 235 return insertIndex; 236 }, 237 238 deleteRule: function(styleSheet, ruleIndex) 239 { 240 if (FBTrace.DBG_CSS) FBTrace.sysout("deleteRule: " + ruleIndex + " " + styleSheet.cssRules.length, styleSheet.cssRules); 241 dispatch(this.fbListeners, "onCSSDeleteRule", [styleSheet, ruleIndex]); 242 243 styleSheet.deleteRule(ruleIndex); 244 }, 245 246 setProperty: function(rule, propName, propValue, propPriority) 247 { 248 var style = rule.style || rule; 249 250 // Record the original CSS text for the inline case so we can reconstruct at a later 251 // point for diffing purposes 252 var baseText = style.cssText; 253 254 var prevValue = style.getPropertyValue(propName); 255 var prevPriority = style.getPropertyPriority(propName); 256 257 // XXXjoe Gecko bug workaround: Just changing priority doesn't have any effect 258 // unless we remove the property first 259 style.removeProperty(propName); 260 261 style.setProperty(propName, propValue, propPriority); 262 263 if (propName) { 264 dispatch(this.fbListeners, "onCSSSetProperty", [style, propName, propValue, propPriority, prevValue, prevPriority, rule, baseText]); 265 } 266 }, 267 268 removeProperty: function(rule, propName, parent) 269 { 270 var style = rule.style || rule; 271 272 // Record the original CSS text for the inline case so we can reconstruct at a later 273 // point for diffing purposes 274 var baseText = style.cssText; 275 276 var prevValue = style.getPropertyValue(propName); 277 var prevPriority = style.getPropertyPriority(propName); 278 279 style.removeProperty(propName); 280 281 if (propName) { 282 dispatch(this.fbListeners, "onCSSRemoveProperty", [style, propName, prevValue, prevPriority, rule, baseText]); 283 } 284 }, 285 286 cleanupSheets: function(doc, context) 287 { 288 // Due to the manner in which the layout engine handles multiple 289 // references to the same sheet we need to kick it a little bit. 290 // The injecting a simple stylesheet then removing it will force 291 // Firefox to regenerate it's CSS hierarchy. 292 // 293 // WARN: This behavior was determined anecdotally. 294 // See http://code.google.com/p/fbug/issues/detail?id=2440 295 if (!isXMLPrettyPrint(doc)) { 296 var style = doc.createElementNS("http://www.w3.org/1999/xhtml", "style"); 297 style.setAttribute("charset","utf-8"); 298 unwrapObject(style).firebugIgnore = true; 299 style.setAttribute("type", "text/css"); 300 style.innerHTML = "#fbIgnoreStyleDO_NOT_USE {}"; 301 addStyleSheet(doc, style); 302 style.parentNode.removeChild(style); 303 } 304 305 // https://bugzilla.mozilla.org/show_bug.cgi?id=500365 306 // This voodoo touches each style sheet to force some Firefox internal change to allow edits. 307 var styleSheets = getAllStyleSheets(context); 308 for(var i = 0; i < styleSheets.length; i++) 309 { 310 try 311 { 312 var rules = styleSheets[i].cssRules; 313 if (rules.length > 0) 314 var touch = rules[0]; 315 if (FBTrace.DBG_CSS && touch) 316 FBTrace.sysout("css.show() touch "+typeof(touch)+" in "+(styleSheets[i].href?styleSheets[i].href:context.getName())); 317 } 318 catch(e) 319 { 320 if (FBTrace.DBG_ERRORS) 321 FBTrace.sysout("css.show: sheet.cssRules FAILS for "+(styleSheets[i]?styleSheets[i].href:"null sheet")+e, e); 322 } 323 } 324 }, 325 cleanupSheetHandler: function(event, context) 326 { 327 var target = event.target, 328 tagName = (target.tagName || "").toLowerCase(); 329 if (tagName == "link") 330 { 331 this.cleanupSheets(target.ownerDocument, context); 332 } 333 }, 334 watchWindow: function(context, win) 335 { 336 var cleanupSheets = bind(this.cleanupSheets, this), 337 cleanupSheetHandler = bind(this.cleanupSheetHandler, this, context), 338 doc = win.document; 339 340 doc.addEventListener("DOMAttrModified", cleanupSheetHandler, false); 341 doc.addEventListener("DOMNodeInserted", cleanupSheetHandler, false); 342 }, 343 loadedContext: function(context) 344 { 345 var self = this; 346 iterateWindows(context.browser.contentWindow, function(subwin) 347 { 348 self.cleanupSheets(subwin.document, context); 349 }); 350 } 351 }); 352 353 // ************************************************************************************************ 354 355 Firebug.CSSStyleSheetPanel = function() {} 356 357 Firebug.CSSStyleSheetPanel.prototype = extend(Firebug.SourceBoxPanel, 358 { 359 template: domplate( 360 { 361 tag: 362 DIV({class: "cssSheet insertInto a11yCSSView"}, 363 FOR("rule", "$rules", 364 CSSRuleTag 365 ), 366 DIV({class: "cssSheet editable insertBefore"}, "") 367 ) 368 }), 369 370 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 371 372 refresh: function() 373 { 374 if (this.location) 375 this.updateLocation(this.location); 376 else if (this.selection) 377 this.updateSelection(this.selection); 378 }, 379 380 toggleEditing: function() 381 { 382 if (!this.stylesheetEditor) 383 this.stylesheetEditor = new StyleSheetEditor(this.document); 384 385 if (this.editing) 386 Firebug.Editor.stopEditing(); 387 else 388 { 389 if (!this.location) 390 return; 391 392 var styleSheet = this.location.editStyleSheet 393 ? this.location.editStyleSheet.sheet 394 : this.location; 395 396 var css = getStyleSheetCSS(styleSheet, this.context); 397 //var topmost = getTopmostRuleLine(this.panelNode); 398 399 this.stylesheetEditor.styleSheet = this.location; 400 Firebug.Editor.startEditing(this.panelNode, css, this.stylesheetEditor); 401 //this.stylesheetEditor.scrollToLine(topmost.line, topmost.offset); 402 } 403 }, 404 405 getStylesheetURL: function(rule) 406 { 407 if (this.location.href) 408 return this.location.href; 409 else 410 return this.context.window.location.href; 411 }, 412 413 getRuleByLine: function(styleSheet, line) 414 { 415 if (!domUtils) 416 return null; 417 418 var cssRules = styleSheet.cssRules; 419 for (var i = 0; i < cssRules.length; ++i) 420 { 421 var rule = cssRules[i]; 422 if (rule instanceof CSSStyleRule) 423 { 424 var ruleLine = domUtils.getRuleLine(rule); 425 if (ruleLine >= line) 426 return rule; 427 } 428 } 429 }, 430 431 highlightRule: function(rule) 432 { 433 var ruleElement = Firebug.getElementByRepObject(this.panelNode.firstChild, rule); 434 if (ruleElement) 435 { 436 scrollIntoCenterView(ruleElement, this.panelNode); 437 setClassTimed(ruleElement, "jumpHighlight", this.context); 438 } 439 }, 440 441 getStyleSheetRules: function(context, styleSheet) 442 { 443 var isSystemSheet = isSystemStyleSheet(styleSheet); 444 445 function appendRules(cssRules) 446 { 447 for (var i = 0; i < cssRules.length; ++i) 448 { 449 var rule = cssRules[i]; 450 if (rule instanceof CSSStyleRule) 451 { 452 var props = this.getRuleProperties(context, rule); 453 var line = domUtils.getRuleLine(rule); 454 var ruleId = rule.selectorText+"/"+line; 455 rules.push({tag: CSSStyleRuleTag.tag, rule: rule, id: ruleId, 456 selector: rule.selectorText, props: props, 457 isSystemSheet: isSystemSheet, 458 isSelectorEditable: true}); 459 } 460 else if (rule instanceof CSSImportRule) 461 rules.push({tag: CSSImportRuleTag.tag, rule: rule}); 462 else if (rule instanceof CSSMediaRule) 463 appendRules.apply(this, [rule.cssRules]); 464 else 465 { 466 if (FBTrace.DBG_ERRORS || FBTrace.DBG_CSS) 467 FBTrace.sysout("css getStyleSheetRules failed to classify a rule ", rule); 468 } 469 } 470 } 471 472 var rules = []; 473 appendRules.apply(this, [styleSheet.cssRules]); 474 return rules; 475 }, 476 477 parseCSSProps: function(style, inheritMode) 478 { 479 var props = []; 480 481 if (Firebug.expandShorthandProps) 482 { 483 var count = style.length-1, 484 index = style.length; 485 while (index--) 486 { 487 var propName = style.item(count - index); 488 this.addProperty(propName, style.getPropertyValue(propName), !!style.getPropertyPriority(propName), false, inheritMode, props); 489 } 490 } 491 else 492 { 493 var lines = style.cssText.match(/(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g); 494 var propRE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(! important)?;?$/; 495 var line,i=0; 496 while(line=lines[i++]){ 497 m = propRE.exec(line); 498 if(!m) 499 continue; 500 //var name = m[1], value = m[2], important = !!m[3]; 501 if (m[2]) 502 this.addProperty(m[1], m[2], !!m[3], false, inheritMode, props); 503 }; 504 } 505 506 return props; 507 }, 508 getRuleProperties: function(context, rule, inheritMode) 509 { 510 var props = this.parseCSSProps(rule.style, inheritMode); 511 512 line = domUtils.getRuleLine(rule); 513 var ruleId = rule.selectorText+"/"+line; 514 this.addOldProperties(context, ruleId, inheritMode, props); 515 sortProperties(props); 516 517 return props; 518 }, 519 520 addOldProperties: function(context, ruleId, inheritMode, props) 521 { 522 if (context.selectorMap && context.selectorMap.hasOwnProperty(ruleId) ) 523 { 524 var moreProps = context.selectorMap[ruleId]; 525 for (var i = 0; i < moreProps.length; ++i) 526 { 527 var prop = moreProps[i]; 528 this.addProperty(prop.name, prop.value, prop.important, true, inheritMode, props); 529 } 530 } 531 }, 532 533 addProperty: function(name, value, important, disabled, inheritMode, props) 534 { 535 if (inheritMode && !inheritedStyleNames[name]) 536 return; 537 538 name = this.translateName(name, value); 539 if (name) 540 { 541 value = stripUnits(rgbToHex(value)); 542 important = important ? " !important" : ""; 543 544 var prop = {name: name, value: value, important: important, disabled: disabled}; 545 props.push(prop); 546 } 547 }, 548 549 translateName: function(name, value) 550 { 551 // Don't show these proprietary Mozilla properties 552 if ((value == "-moz-initial" 553 && (name == "-moz-background-clip" || name == "-moz-background-origin" 554 || name == "-moz-background-inline-policy")) 555 || (value == "physical" 556 && (name == "margin-left-ltr-source" || name == "margin-left-rtl-source" 557 || name == "margin-right-ltr-source" || name == "margin-right-rtl-source")) 558 || (value == "physical" 559 && (name == "padding-left-ltr-source" || name == "padding-left-rtl-source" 560 || name == "padding-right-ltr-source" || name == "padding-right-rtl-source"))) 561 return null; 562 563 // Translate these back to the form the user probably expects 564 if (name == "margin-left-value") 565 return "margin-left"; 566 else if (name == "margin-right-value") 567 return "margin-right"; 568 else if (name == "margin-top-value") 569 return "margin-top"; 570 else if (name == "margin-bottom-value") 571 return "margin-bottom"; 572 else if (name == "padding-left-value") 573 return "padding-left"; 574 else if (name == "padding-right-value") 575 return "padding-right"; 576 else if (name == "padding-top-value") 577 return "padding-top"; 578 else if (name == "padding-bottom-value") 579 return "padding-bottom"; 580 // XXXjoe What about border! 581 else 582 return name; 583 }, 584 585 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 586 587 editElementStyle: function() 588 { 589 var rulesBox = this.panelNode.getElementsByClassName("cssElementRuleContainer")[0]; 590 var styleRuleBox = rulesBox && Firebug.getElementByRepObject(rulesBox, this.selection); 591 if (!styleRuleBox) 592 { 593 var rule = {rule: this.selection, inherited: false, selector: "element.style", props: []}; 594 if (!rulesBox) 595 { 596 // The element did not have any displayed styles. We need to create the whole tree and remove 597 // the no styles message 598 styleRuleBox = this.template.cascadedTag.replace({ 599 rules: [rule], inherited: [], inheritLabel: $STR("InheritedFrom") 600 }, this.panelNode); 601 602 styleRuleBox = styleRuleBox.getElementsByClassName("cssElementRuleContainer")[0]; 603 } 604 else 605 styleRuleBox = this.template.ruleTag.insertBefore({rule: rule}, rulesBox); 606 607 styleRuleBox = styleRuleBox.getElementsByClassName("insertInto")[0]; 608 } 609 610 Firebug.Editor.insertRowForObject(styleRuleBox); 611 }, 612 613 insertPropertyRow: function(row) 614 { 615 Firebug.Editor.insertRowForObject(row); 616 }, 617 618 insertRule: function(row) 619 { 620 var location = getAncestorByClass(row, "cssRule"); 621 if (!location) 622 { 623 location = getChildByClass(this.panelNode, "cssSheet"); 624 Firebug.Editor.insertRowForObject(location); 625 } 626 else 627 { 628 Firebug.Editor.insertRow(location, "before"); 629 } 630 }, 631 632 editPropertyRow: function(row) 633 { 634 var propValueBox = getChildByClass(row, "cssPropValue"); 635 Firebug.Editor.startEditing(propValueBox); 636 }, 637 638 deletePropertyRow: function(row) 639 { 640 var rule = Firebug.getRepObject(row); 641 var propName = getChildByClass(row, "cssPropName").textContent; 642 Firebug.CSSModule.removeProperty(rule, propName); 643 644 // Remove the property from the selector map, if it was disabled 645 var ruleId = Firebug.getRepNode(row).getAttribute("ruleId"); 646 if ( this.context.selectorMap && this.context.selectorMap.hasOwnProperty(ruleId) ) 647 { 648 var map = this.context.selectorMap[ruleId]; 649 for (var i = 0; i < map.length; ++i) 650 { 651 if (map[i].name == propName) 652 { 653 map.splice(i, 1); 654 break; 655 } 656 } 657 } 658 if (this.name == "stylesheet") 659 dispatch([Firebug.A11yModel], 'onInlineEditorClose', [this, row.firstChild, true]); 660 row.parentNode.removeChild(row); 661 662 this.markChange(this.name == "stylesheet"); 663 }, 664 665 disablePropertyRow: function(row) 666 { 667 toggleClass(row, "disabledStyle"); 668 669 var rule = Firebug.getRepObject(row); 670 var propName = getChildByClass(row, "cssPropName").textContent; 671 672 if (!this.context.selectorMap) 673 this.context.selectorMap = {}; 674 675 // XXXjoe Generate unique key for elements too 676 var ruleId = Firebug.getRepNode(row).getAttribute("ruleId"); 677 if (!(this.context.selectorMap.hasOwnProperty(ruleId))) 678 this.context.selectorMap[ruleId] = []; 679 680 var map = this.context.selectorMap[ruleId]; 681 var propValue = getChildByClass(row, "cssPropValue").textContent; 682 var parsedValue = parsePriority(propValue); 683 if (hasClass(row, "disabledStyle")) 684 { 685 Firebug.CSSModule.removeProperty(rule, propName); 686 687 map.push({"name": propName, "value": parsedValue.value, 688 "important": parsedValue.priority}); 689 } 690 else 691 { 692 Firebug.CSSModule.setProperty(rule, propName, parsedValue.value, parsedValue.priority); 693 694 var index = findPropByName(map, propName); 695 map.splice(index, 1); 696 } 697 698 this.markChange(this.name == "stylesheet"); 699 }, 700 701 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 702 703 onMouseDown: function(event) 704 { 705 // XXjoe Hack to only allow clicking on the checkbox 706 if (!isLeftClick(event) || event.clientX > 20) 707 return; 708 709 if (hasClass(event.target, "textEditor")) 710 return; 711 712 var row = getAncestorByClass(event.target, "cssProp"); 713 if (row && hasClass(row, "editGroup")) 714 { 715 this.disablePropertyRow(row); 716 cancelEvent(event); 717 } 718 }, 719 720 onClick: function(event) 721 { 722 if (!isLeftClick(event) || event.clientX <= 20 || event.detail != 2) 723 return; 724 725 var row = getAncestorByClass(event.target, "cssRule"); 726 if (row && !getAncestorByClass(event.target, "cssPropName") 727 && !getAncestorByClass(event.target, "cssPropValue")) 728 { 729 this.insertPropertyRow(row); 730 cancelEvent(event); 731 } 732 }, 733 734 735 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 736 // extends Panel 737 738 name: "stylesheet", 739 parentPanel: null, 740 searchable: true, 741 dependents: ["css", "stylesheet", "dom", "domSide", "layout"], 742 743 initialize: function() 744 { 745 this.onMouseDown = bind(this.onMouseDown, this); 746 this.onClick = bind(this.onClick, this); 747 748 Firebug.SourceBoxPanel.initialize.apply(this, arguments); 749 }, 750 751 destroy: function(state) 752 { 753 state.scrollTop = this.panelNode.scrollTop ? this.panelNode.scrollTop : this.lastScrollTop; 754 755 persistObjects(this, state); 756 757 Firebug.Editor.stopEditing(); 758 Firebug.Panel.destroy.apply(this, arguments); 759 }, 760 761 initializeNode: function(oldPanelNode) 762 { 763 this.panelNode.addEventListener("mousedown", this.onMouseDown, false); 764 this.panelNode.addEventListener("click", this.onClick, false); 765 Firebug.SourceBoxPanel.initializeNode.apply(this, arguments); 766 dispatch([Firebug.A11yModel], 'onInitializeNode', [this, 'css']); 767 }, 768 769 destroyNode: function() 770 { 771 this.panelNode.removeEventListener("mousedown", this.onMouseDown, false); 772 this.panelNode.removeEventListener("click", this.onClick, false); 773 Firebug.SourceBoxPanel.destroyNode.apply(this, arguments); 774 dispatch([Firebug.A11yModel], 'onDestroyNode', [this, 'css']); 775 }, 776 777 show: function(state) 778 { 779 Firebug.Inspector.stopInspecting(true); 780 781 this.showToolbarButtons("fbCSSButtons", true); 782 783 if (this.context.loaded && !this.location) // wait for loadedContext to restore the panel 784 { 785 restoreObjects(this, state); 786 787 if (!this.location) 788 this.location = this.getDefaultLocation(); 789 790 if (state && state.scrollTop) 791 this.panelNode.scrollTop = state.scrollTop; 792 } 793 }, 794 795 hide: function() 796 { 797 this.showToolbarButtons("fbCSSButtons", false); 798 799 this.lastScrollTop = this.panelNode.scrollTop; 800 }, 801 802 supportsObject: function(object) 803 { 804 if (object instanceof CSSStyleSheet) 805 return 1; 806 else if (object instanceof CSSStyleRule) 807 return 2; 808 else if (object instanceof CSSStyleDeclaration) 809 return 2; 810 else if (object instanceof SourceLink && object.type == "css" && reCSS.test(object.href)) 811 return 2; 812 else 813 return 0; 814 }, 815 816 updateLocation: function(styleSheet) 817 { 818 if (FBTrace.DBG_CSS) 819 FBTrace.sysout("css.updateLocation; " + (styleSheet ? styleSheet.href : "no stylesheet")); 820 821 if (!styleSheet) 822 return; 823 824 if (styleSheet.editStyleSheet) 825 styleSheet = styleSheet.editStyleSheet.sheet; 826 827 var rules = this.getStyleSheetRules(this.context, styleSheet); 828 829 var result; 830 if (rules.length) 831 result = this.template.tag.replace({rules: rules}, this.panelNode); 832 else 833 result = FirebugReps.Warning.tag.replace({object: "EmptyStyleSheet"}, this.panelNode); 834 835 this.showToolbarButtons("fbCSSButtons", !isSystemStyleSheet(this.location)); 836 837 dispatch([Firebug.A11yModel], 'onCSSRulesAdded', [this, this.panelNode]); 838 839 // If the full editing mode (not the inline) is on while the location changes, 840 // open the editor again for another file. 841 if (this.editing && this.stylesheetEditor && this.stylesheetEditor.editing) 842 { 843 // Remove the editing flag to avoid recursion. The StylesheetEditor.endEditing 844 // calls refresh and consequently updateLocation of the CSS panel. 845 this.editing = null; 846 847 // Stop the current editing. 848 Firebug.Editor.stopEditing(); 849 850 // ... and open the editor again. 851 this.toggleEditing(); 852 } 853 }, 854 855 updateSelection: function(object) 856 { 857 this.selection = null; 858 859 if (object instanceof CSSStyleDeclaration) { 860 object = object.parentRule; 861 } 862 863 if (object instanceof CSSStyleRule) 864 { 865 this.navigate(object.parentStyleSheet); 866 this.highlightRule(object); 867 } 868 else if (object instanceof CSSStyleSheet) 869 { 870 this.navigate(object); 871 } 872 else if (object instanceof SourceLink) 873 { 874 try 875 { 876 var sourceLink = object; 877 878 var sourceFile = getSourceFileByHref(sourceLink.href, this.context); 879 if (sourceFile) 880 { 881 clearNode(this.panelNode); // replace rendered stylesheets 882 this.showSourceFile(sourceFile); 883 884 var lineNo = object.line; 885 if (lineNo) 886 this.scrollToLine(lineNo, this.jumpHighlightFactory(lineNo, this.context)); 887 } 888 else // XXXjjb we should not be taking this path 889 { 890 var stylesheet = getStyleSheetByHref(sourceLink.href, this.context); 891 if (stylesheet) 892 this.navigate(stylesheet); 893 else 894 { 895 if (FBTrace.DBG_CSS) 896 FBTrace.sysout("css.updateSelection no sourceFile for "+sourceLink.href, sourceLink); 897 } 898 } 899 } 900 catch(exc) { 901 if (FBTrace.DBG_CSS) 902 FBTrace.sysout("css.upDateSelection FAILS "+exc, exc); 903 } 904 } 905 }, 906 907 updateOption: function(name, value) 908 { 909 if (name == "expandShorthandProps") 910 this.refresh(); 911 }, 912 913 getLocationList: function() 914 { 915 var styleSheets = getAllStyleSheets(this.context); 916 return styleSheets; 917 }, 918 919 getOptionsMenuItems: function() 920 { 921 return [ 922 {label: "Expand Shorthand Properties", type: "checkbox", checked: Firebug.expandShorthandProps, 923 command: bindFixed(Firebug.togglePref, Firebug, "expandShorthandProps") }, 924 "-", 925 {label: "Refresh", command: bind(this.refresh, this) } 926 ]; 927 }, 928 929 getContextMenuItems: function(style, target) 930 { 931 var items = []; 932 933 if (this.infoTipType == "color") 934 { 935 items.push( 936 {label: "CopyColor", 937 command: bindFixed(copyToClipboard, FBL, this.infoTipObject) } 938 ); 939 } 940 else if (this.infoTipType == "image") 941 { 942 items.push( 943 {label: "CopyImageLocation", 944 command: bindFixed(copyToClipboard, FBL, this.infoTipObject) }, 945 {label: "OpenImageInNewTab", 946 command: bindFixed(openNewTab, FBL, this.infoTipObject) } 947 ); 948 } 949 950 if (this.selection instanceof Element) 951 { 952 items.push( 953 "-", 954 {label: "EditStyle", 955 command: bindFixed(this.editElementStyle, this) } 956 ); 957 } 958 else if (!isSystemStyleSheet(this.selection)) 959 { 960 items.push( 961 "-", 962 {label: "NewRule", 963 command: bindFixed(this.insertRule, this, target) } 964 ); 965 } 966 967 var cssRule = getAncestorByClass(target, "cssRule") 968 if (cssRule && hasClass(cssRule, "cssEditableRule")) 969 { 970 items.push( 971 "-", 972 {label: "NewProp", 973 command: bindFixed(this.insertPropertyRow, this, target) } 974 ); 975 976 var propRow = getAncestorByClass(target, "cssProp"); 977 if (propRow) 978 { 979 var propName = getChildByClass(propRow, "cssPropName").textContent; 980 var isDisabled = hasClass(propRow, "disabledStyle"); 981 982 items.push( 983 {label: $STRF("EditProp", [propName]), nol10n: true, 984 command: bindFixed(this.editPropertyRow, this, propRow) }, 985 {label: $STRF("DeleteProp", [propName]), nol10n: true, 986 command: bindFixed(this.deletePropertyRow, this, propRow) }, 987 {label: $STRF("DisableProp", [propName]), nol10n: true, 988 type: "checkbox", checked: isDisabled, 989 command: bindFixed(this.disablePropertyRow, this, propRow) } 990 ); 991 } 992 } 993 994 items.push( 995 "-", 996 {label: "Refresh", command: bind(this.refresh, this) } 997 ); 998 999 return items; 1000 }, 1001 1002 browseObject: function(object) 1003 { 1004 if (this.infoTipType == "image") 1005 { 1006 openNewTab(this.infoTipObject); 1007 return true; 1008 } 1009 }, 1010 1011 showInfoTip: function(infoTip, target, x, y) 1012 { 1013 var propValue = getAncestorByClass(target, "cssPropValue"); 1014 if (propValue) 1015 { 1016 var offset = getClientOffset(propValue); 1017 var offsetX = x-offset.x; 1018 1019 var text = propValue.textContent; 1020 var charWidth = propValue.offsetWidth/text.length; 1021 var charOffset = Math.floor(offsetX/charWidth); 1022 1023 var cssValue = parseCSSValue(text, charOffset); 1024 if (cssValue) 1025 { 1026 if (cssValue.value == this.infoTipValue) 1027 return true; 1028 1029 this.infoTipValue = cssValue.value; 1030 1031 if (cssValue.type == "rgb" || (!cssValue.type && isColorKeyword(cssValue.value))) 1032 { 1033 this.infoTipType = "color"; 1034 this.infoTipObject = cssValue.value; 1035 1036 return Firebug.InfoTip.populateColorInfoTip(infoTip, cssValue.value); 1037 } 1038 else if (cssValue.type == "url") 1039 { 1040 var propNameNode = target.parentNode.getElementsByClassName("cssPropName").item(0); 1041 if (propNameNode && isImageRule(propNameNode.textContent)) 1042 { 1043 var rule = Firebug.getRepObject(target); 1044 var baseURL = this.getStylesheetURL(rule); 1045 var relURL = parseURLValue(cssValue.value); 1046 var absURL = isDataURL(relURL) ? relURL:absoluteURL(relURL, baseURL); 1047 var repeat = parseRepeatValue(text); 1048 1049 this.infoTipType = "image"; 1050 this.infoTipObject = absURL; 1051 1052 return Firebug.InfoTip.populateImageInfoTip(infoTip, absURL, repeat); 1053 } 1054 } 1055 } 1056 } 1057 1058 delete this.infoTipType; 1059 delete this.infoTipValue; 1060 delete this.infoTipObject; 1061 }, 1062 1063 getEditor: function(target, value) 1064 { 1065 if (target == this.panelNode 1066 || hasClass(target, "cssSelector") || hasClass(target, "cssRule") 1067 || hasClass(target, "cssSheet")) 1068 { 1069 if (!this.ruleEditor) 1070 this.ruleEditor = new CSSRuleEditor(this.document); 1071 1072 return this.ruleEditor; 1073 } 1074 else 1075 { 1076 if (!this.editor) 1077 this.editor = new CSSEditor(this.document); 1078 1079 return this.editor; 1080 } 1081 }, 1082 1083 getDefaultLocation: function() 1084 { 1085 try 1086 { 1087 var styleSheets = this.context.window.document.styleSheets; 1088 if (styleSheets.length) 1089 { 1090 var sheet = styleSheets[0]; 1091 return (Firebug.filterSystemURLs && isSystemURL(getURLForStyleSheet(sheet))) ? null : sheet; 1092 } 1093 } 1094 catch (exc) 1095 { 1096 if (FBTrace.DBG_LOCATIONS) 1097 FBTrace.sysout("css.getDefaultLocation FAILS "+exc, exc); 1098 } 1099 }, 1100 1101 getObjectDescription: function(styleSheet) 1102 { 1103 var url = getURLForStyleSheet(styleSheet); 1104 var instance = getInstanceForStyleSheet(styleSheet); 1105 1106 var baseDescription = splitURLBase(url); 1107 if (instance) { 1108 baseDescription.name = baseDescription.name + " #" + (instance + 1); 1109 } 1110 return baseDescription; 1111 }, 1112 1113 search: function(text, reverse) 1114 { 1115 var curDoc = this.searchCurrentDoc(!Firebug.searchGlobal, text, reverse); 1116 if (!curDoc && Firebug.searchGlobal) 1117 { 1118 return this.searchOtherDocs(text, reverse); 1119 } 1120 return curDoc; 1121 }, 1122 1123 searchOtherDocs: function(text, reverse) 1124 { 1125 var scanRE = Firebug.Search.getTestingRegex(text); 1126 function scanDoc(styleSheet) { 1127 // we don't care about reverse here as we are just looking for existence, 1128 // if we do have a result we will handle the reverse logic on display 1129 for (var i = 0; i < styleSheet.cssRules.length; i++) 1130 { 1131 if (scanRE.test(styleSheet.cssRules[i].cssText)) 1132 { 1133 return true; 1134 } 1135 } 1136 } 1137 1138 if (this.navigateToNextDocument(scanDoc, reverse)) 1139 { 1140 return this.searchCurrentDoc(true, text, reverse); 1141 } 1142 }, 1143 1144 searchCurrentDoc: function(wrapSearch, text, reverse) 1145 { 1146 if (!text) 1147 { 1148 delete this.currentSearch; 1149 return false; 1150 } 1151 1152 var row; 1153 if (this.currentSearch && text == this.currentSearch.text) 1154 { 1155 row = this.currentSearch.findNext(wrapSearch, false, reverse, Firebug.Search.isCaseSensitive(text)); 1156 } 1157 else 1158 { 1159 if (this.editing) 1160 { 1161 this.currentSearch = new TextSearch(this.stylesheetEditor.box); 1162 row = this.currentSearch.find(text, reverse, Firebug.Search.isCaseSensitive(text)); 1163 1164 if (row) 1165 { 1166 var sel = this.document.defaultView.getSelection(); 1167 sel.removeAllRanges(); 1168 sel.addRange(this.currentSearch.range); 1169 scrollSelectionIntoView(this); 1170 return true; 1171 } 1172 else 1173 return false; 1174 } 1175 else 1176 { 1177 function findRow(node) { return node.nodeType == 1 ? node : node.parentNode; } 1178 this.currentSearch = new TextSearch(this.panelNode, findRow); 1179 row = this.currentSearch.find(text, reverse, Firebug.Search.isCaseSensitive(text)); 1180 } 1181 } 1182 1183 if (row) 1184 { 1185 this.document.defaultView.getSelection().selectAllChildren(row); 1186 scrollIntoCenterView(row, this.panelNode); 1187 dispatch([Firebug.A11yModel], 'onCSSSearchMatchFound', [this, text, row]); 1188 return true; 1189 } 1190 else 1191 { 1192 dispatch([Firebug.A11yModel], 'onCSSSearchMatchFound', [this, text, null]); 1193 return false; 1194 } 1195 }, 1196 1197 getSearchOptionsMenuItems: function() 1198 { 1199 return [ 1200 Firebug.Search.searchOptionMenu("search.Case_Sensitive", "searchCaseSensitive"), 1201 Firebug.Search.searchOptionMenu("search.Multiple_Files", "searchGlobal") 1202 ]; 1203 } 1204 }); 1205 1206 // ************************************************************************************************ 1207 1208 function CSSElementPanel() {} 1209 1210 CSSElementPanel.prototype = extend(Firebug.CSSStyleSheetPanel.prototype, 1211 { 1212 template: domplate( 1213 { 1214 cascadedTag: 1215 DIV({"class": "a11yCSSView", role : 'presentation'}, 1216 DIV({role : 'list', 'aria-label' : $STR('aria.labels.style rules') }, 1217 FOR("rule", "$rules", 1218 TAG("$ruleTag", {rule: "$rule"}) 1219 ) 1220 ), 1221 DIV({role : "list", 'aria-label' :$STR('aria.labels.inherited style rules')}, 1222 FOR("section", "$inherited", 1223 1224 H1({class: "cssInheritHeader groupHeader focusRow", role : 'listitem' }, 1225 SPAN({class: "cssInheritLabel"}, "$inheritLabel"), 1226 TAG(FirebugReps.Element.shortTag, {object: "$section.element"}) 1227 ), 1228 DIV({role : 'group'}, 1229 FOR("rule", "$section.rules", 1230 TAG("$ruleTag", {rule: "$rule"}) 1231 ) 1232 ) 1233 ) 1234 ) 1235 ), 1236 1237 ruleTag: 1238 DIV({class: "cssElementRuleContainer"}, 1239 TAG(CSSStyleRuleTag.tag, {rule: "$rule"}), 1240 TAG(FirebugReps.SourceLink.tag, {object: "$rule.sourceLink"}) 1241 ) 1242 }), 1243 1244 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1245 1246 updateCascadeView: function(element) 1247 { 1248 dispatch([Firebug.A11yModel], 'onBeforeCSSRulesAdded', [this]); 1249 var rules = [], sections = [], usedProps = {}; 1250 this.getInheritedRules(element, sections, usedProps); 1251 this.getElementRules(element, rules, usedProps); 1252 1253 if (rules.length || sections.length) 1254 { 1255 var inheritLabel = $STR("InheritedFrom"); 1256 var result = this.template.cascadedTag.replace({rules: rules, inherited: sections, 1257 inheritLabel: inheritLabel}, this.panelNode); 1258 dispatch([Firebug.A11yModel], 'onCSSRulesAdded', [this, result]); 1259 } 1260 else 1261 { 1262 var result = FirebugReps.Warning.tag.replace({object: "EmptyElementCSS"}, this.panelNode); 1263 dispatch([Firebug.A11yModel], 'onCSSRulesAdded', [this, result]); 1264 } 1265 }, 1266 1267 getStylesheetURL: function(rule) 1268 { 1269 // if the parentStyleSheet.href is null, CSS std says its inline style 1270 if (rule && rule.parentStyleSheet.href) 1271 return rule.parentStyleSheet.href; 1272 else 1273 return this.selection.ownerDocument.location.href; 1274 }, 1275 1276 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1277 1278 getInheritedRules: function(element, sections, usedProps) 1279 { 1280 var parent = element.parentNode; 1281 if (parent && parent.nodeType == 1) 1282 { 1283 this.getInheritedRules(parent, sections, usedProps); 1284 1285 var rules = []; 1286 this.getElementRules(parent, rules, usedProps, true); 1287 1288 if (rules.length) 1289 sections.splice(0, 0, {element: parent, rules: rules}); 1290 } 1291 }, 1292 1293 getElementRules: function(element, rules, usedProps, inheritMode) 1294 { 1295 var inspectedRules, displayedRules = {}; 1296 try 1297 { 1298 inspectedRules = domUtils ? domUtils.getCSSStyleRules(element) : null; 1299 } catch (exc) {} 1300 1301 if (inspectedRules) 1302 { 1303 for (var i = 0; i < inspectedRules.Count(); ++i) 1304 { 1305 var rule = QI(inspectedRules.GetElementAt(i), nsIDOMCSSStyleRule); 1306 1307 var href = rule.parentStyleSheet.href; // Null means inline 1308 1309 var instance = getInstanceForStyleSheet(rule.parentStyleSheet, element.ownerDocument); 1310 1311 var isSystemSheet = isSystemStyleSheet(rule.parentStyleSheet); 1312 if (!Firebug.showUserAgentCSS && isSystemSheet) // This removes user agent rules 1313 continue; 1314 if (!href) 1315 href = element.ownerDocument.location.href; // http://code.google.com/p/fbug/issues/detail?id=452 1316 1317 var props = this.getRuleProperties(this.context, rule, inheritMode); 1318 if (inheritMode && !props.length) 1319 continue; 1320 1321 var line = domUtils.getRuleLine(rule); 1322 var ruleId = rule.selectorText+"/"+line; 1323 var sourceLink = new SourceLink(href, line, "css", rule, instance); 1324 1325 this.markOverridenProps(props, usedProps, inheritMode); 1326 1327 rules.splice(0, 0, {rule: rule, id: ruleId, 1328 selector: rule.selectorText, sourceLink: sourceLink, 1329 props: props, inherited: inheritMode, 1330 isSystemSheet: isSystemSheet}); 1331 } 1332 } 1333 1334 if (element.style) 1335 this.getStyleProperties(element, rules, usedProps, inheritMode); 1336 1337 if (FBTrace.DBG_CSS) 1338 FBTrace.sysout("getElementRules "+rules.length+" rules for "+getElementXPath(element), rules); 1339 }, 1340 1341 markOverridenProps: function(props, usedProps, inheritMode) 1342 { 1343 for (var i = 0; i < props.length; ++i) 1344 { 1345 var prop = props[i]; 1346 if ( usedProps.hasOwnProperty(prop.name) ) 1347 { 1348 var deadProps = usedProps[prop.name]; // all previous occurrences of this property 1349 for (var j = 0; j < deadProps.length; ++j) 1350 { 1351 var deadProp = deadProps[j]; 1352 if (!deadProp.disabled && !deadProp.wasInherited && deadProp.important && !prop.important) 1353 prop.overridden = true; // new occurrence overridden 1354 else if (!prop.disabled) 1355 deadProp.overridden = true; // previous occurrences overridden 1356 } 1357 } 1358 else 1359 usedProps[prop.name] = []; 1360 1361 prop.wasInherited = inheritMode ? true : false; 1362 usedProps[prop.name].push(prop); // all occurrences of a property seen so far, by name 1363 } 1364 }, 1365 1366 getStyleProperties: function(element, rules, usedProps, inheritMode) 1367 { 1368 var props = this.parseCSSProps(element.style, inheritMode); 1369 this.addOldProperties(this.context, getElementXPath(element), inheritMode, props); 1370 1371 sortProperties(props); 1372 this.markOverridenProps(props, usedProps, inheritMode); 1373 1374 if (props.length) 1375 rules.splice(0, 0, 1376 {rule: element, id: getElementXPath(element), 1377 selector: "element.style", props: props, inherited: inheritMode}); 1378 }, 1379 1380 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1381 // extends Panel 1382 1383 name: "css", 1384 parentPanel: "html", 1385 order: 0, 1386 1387 initialize: function() 1388 { 1389 Firebug.CSSStyleSheetPanel.prototype.initialize.apply(this, arguments); 1390 1391 this.onStateChange = bindFixed(this.contentStateCheck, this); 1392 this.onHoverChange = bindFixed(this.contentStateCheck, this, STATE_HOVER); 1393 this.onActiveChange = bindFixed(this.contentStateCheck, this, STATE_ACTIVE); 1394 }, 1395 1396 show: function(state) 1397 { 1398 }, 1399 1400 watchWindow: function(win) 1401 { 1402 if (domUtils) 1403 { 1404 // Normally these would not be required, but in order to update after the state is set 1405 // using the options menu we need to monitor these global events as well 1406 var doc = win.document; 1407 doc.addEventListener("mouseover", this.onHoverChange, false); 1408 doc.addEventListener("mousedown", this.onActiveChange, false); 1409 } 1410 }, 1411 unwatchWindow: function(win) 1412 { 1413 var doc = win.document; 1414 doc.removeEventListener("mouseover", this.onHoverChange, false); 1415 doc.removeEventListener("mousedown", this.onActiveChange, false); 1416 1417 if (isAncestor(this.stateChangeEl, doc)) 1418 { 1419 this.removeStateChangeHandlers(); 1420 } 1421 }, 1422 1423 supportsObject: function(object) 1424 { 1425 return object instanceof Element ? 1 : 0; 1426 }, 1427 1428 updateView: function(element) 1429 { 1430 this.updateCascadeView(element); 1431 if (domUtils) 1432 { 1433 this.contentState = safeGetContentState(element); 1434 this.addStateChangeHandlers(element); 1435 } 1436 }, 1437 1438 updateSelection: function(element) 1439 { 1440 if ( !(element instanceof Element) ) // html supports SourceLink 1441 return; 1442 1443 if (sothinkInstalled) 1444 { 1445 FirebugReps.Warning.tag.replace({object: "SothinkWarning"}, this.panelNode); 1446 return; 1447 } 1448 1449 if (!domUtils) 1450 { 1451 FirebugReps.Warning.tag.replace({object: "DOMInspectorWarning"}, this.panelNode); 1452 return; 1453 } 1454 1455 if (!element) 1456 return; 1457 1458 this.updateView(element); 1459 }, 1460 1461 updateOption: function(name, value) 1462 { 1463 if (name == "showUserAgentCSS" || name == "expandShorthandProps") 1464 this.refresh(); 1465 }, 1466 1467 getOptionsMenuItems: function() 1468 { 1469 var ret = [ 1470 {label: "Show User Agent CSS", type: "checkbox", checked: Firebug.showUserAgentCSS, 1471 command: bindFixed(Firebug.togglePref, Firebug, "showUserAgentCSS") }, 1472 {label: "Expand Shorthand Properties", type: "checkbox", checked: Firebug.expandShorthandProps, 1473 command: bindFixed(Firebug.togglePref, Firebug, "expandShorthandProps") } 1474 ]; 1475 if (domUtils && this.selection) 1476 { 1477 var state = safeGetContentState(this.selection); 1478 1479 ret.push("-"); 1480 ret.push({label: ":active", type: "checkbox", checked: state & STATE_ACTIVE, 1481 command: bindFixed(this.updateContentState, this, STATE_ACTIVE, state & STATE_ACTIVE)}); 1482 ret.push({label: ":hover", type: "checkbox", checked: state & STATE_HOVER, 1483 command: bindFixed(this.updateContentState, this, STATE_HOVER, state & STATE_HOVER)}); 1484 } 1485 return ret; 1486 }, 1487 1488 updateContentState: function(state, remove) 1489 { 1490 domUtils.setContentState(remove ? this.selection.ownerDocument.documentElement : this.selection, state); 1491 this.refresh(); 1492 }, 1493 1494 addStateChangeHandlers: function(el) 1495 { 1496 this.removeStateChangeHandlers(); 1497 1498 el.addEventListener("focus", this.onStateChange, true); 1499 el.addEventListener("blur", this.onStateChange, true); 1500 el.addEventListener("mouseup", this.onStateChange, false); 1501 el.addEventListener("mousedown", this.onStateChange, false); 1502 el.addEventListener("mouseover", this.onStateChange, false); 1503 el.addEventListener("mouseout", this.onStateChange, false); 1504 1505 this.stateChangeEl = el; 1506 }, 1507 1508 removeStateChangeHandlers: function() 1509 { 1510 var sel = this.stateChangeEl; 1511 if (sel) 1512 { 1513 sel.removeEventListener("focus", this.onStateChange, true); 1514 sel.removeEventListener("blur", this.onStateChange, true); 1515 sel.removeEventListener("mouseup", this.onStateChange, false); 1516 sel.removeEventListener("mousedown", this.onStateChange, false); 1517 sel.removeEventListener("mouseover", this.onStateChange, false); 1518 sel.removeEventListener("mouseout", this.onStateChange, false); 1519 } 1520 }, 1521 1522 contentStateCheck: function(state) 1523 { 1524 if (!state || this.contentState & state) 1525 { 1526 var timeoutRunner = bindFixed(function() 1527 { 1528 var newState = safeGetContentState(this.selection); 1529 if (newState != this.contentState) 1530 { 1531 this.context.invalidatePanels(this.name); 1532 } 1533 }, this); 1534 1535 // Delay exec until after the event has processed and the state has been updated 1536 setTimeout(timeoutRunner, 0); 1537 } 1538 } 1539 }); 1540 1541 function safeGetContentState(selection) 1542 { 1543 try 1544 { 1545 return domUtils.getContentState(selection); 1546 } 1547 catch (e) 1548 { 1549 if (FBTrace.DBG_ERRORS) 1550 FBTrace.sysout("css.safeGetContentState; EXCEPTION "+e, e); 1551 } 1552 } 1553 1554 function isXMLPrettyPrint(doc) 1555 { 1556 try 1557 { 1558 var bindings = domUtils.getBindingURLs(doc.documentElement); 1559 for (var i = 0; i < bindings.length; i++) 1560 { 1561 var bindingURI = bindings.queryElementAt(i, nsIURI); 1562 if (FBTrace.DBG_CSS) { FBTrace.sysout("bindingURL: " + i + " " + bindingURI.resolve("")); } 1563 if (bindingURI.resolve("") === "chrome://global/content/xml/XMLPrettyPrint.xml") 1564 return true; 1565 } 1566 } 1567 catch (e) 1568 { 1569 if (FBTrace.DBG_ERRORS) 1570 FBTrace.sysout("css.isXMLPrettyPrint; EXCEPTION "+e, e); 1571 } 1572 } 1573 1574 // ************************************************************************************************ 1575 1576 function CSSComputedElementPanel() {} 1577 1578 CSSComputedElementPanel.prototype = extend(CSSElementPanel.prototype, 1579 { 1580 template: domplate( 1581 { 1582 computedTag: 1583 DIV({"class": "a11yCSSView", role : "list", "aria-label" : $STR('aria.labels.computed styles')}, 1584 FOR("group", "$groups", 1585 H1({class: "cssInheritHeader groupHeader focusRow", role : "listitem"}, 1586 SPAN({class: "cssInheritLabel"}, "$group.title") 1587 ), 1588 TABLE({width: "100%", role : 'group'}, 1589 TBODY({role : 'presentation'}, 1590 FOR("prop", "$group.props", 1591 TR({class : 'focusRow computedStyleRow', role : 'listitem'}, 1592 TD({class: "stylePropName", role : 'presentation'}, "$prop.name"), 1593 TD({class: "stylePropValue", role : 'presentation'}, "$prop.value") 1594 ) 1595 ) 1596 ) 1597 ) 1598 ) 1599 ) 1600 }), 1601 1602 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1603 1604 updateComputedView: function(element) 1605 { 1606 var win = element.ownerDocument.defaultView; 1607 var style = win.getComputedStyle(element, ""); 1608 1609 var groups = []; 1610 1611 for (var groupName in styleGroups) 1612 { 1613 var title = $STR("StyleGroup-" + groupName); 1614 var group = {title: title, props: []}; 1615 groups.push(group); 1616 1617 var props = styleGroups[groupName]; 1618 for (var i = 0; i < props.length; ++i) 1619 { 1620 var propName = props[i]; 1621 var propValue = stripUnits(rgbToHex(style.getPropertyValue(propName))); 1622 if (propValue) 1623 group.props.push({name: propName, value: propValue}); 1624 } 1625 } 1626 1627 var result = this.template.computedTag.replace({groups: groups}, this.panelNode); 1628 dispatch([Firebug.A11yModel], 'onCSSRulesAdded', [this, result]); 1629 }, 1630 1631 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1632 // extends Panel 1633 1634 name: "computed", 1635 parentPanel: "html", 1636 order: 1, 1637 1638 updateView: function(element) 1639 { 1640 this.updateComputedView(element); 1641 }, 1642 1643 getOptionsMenuItems: function() 1644 { 1645 return [ 1646 {label: "Refresh", command: bind(this.refresh, this) } 1647 ]; 1648 } 1649 }); 1650 1651 // ************************************************************************************************ 1652 // CSSEditor 1653 1654 function CSSEditor(doc) 1655 { 1656 this.initializeInline(doc); 1657 } 1658 1659 CSSEditor.prototype = domplate(Firebug.InlineEditor.prototype, 1660 { 1661 insertNewRow: function(target, insertWhere) 1662 { 1663 var rule = Firebug.getRepObject(target); 1664 var emptyProp = {name: "", value: "", important: ""}; 1665 1666 if (insertWhere == "before") 1667 return CSSPropTag.tag.insertBefore({prop: emptyProp, rule: rule}, target); 1668 else 1669 return CSSPropTag.tag.insertAfter({prop: emptyProp, rule: rule}, target); 1670 }, 1671 1672 saveEdit: function(target, value, previousValue) 1673 { 1674 target.innerHTML = escapeForCss(value); 1675 1676 var row = getAncestorByClass(target, "cssProp"); 1677 if (hasClass(row, "disabledStyle")) 1678 toggleClass(row, "disabledStyle"); 1679 1680 var rule = Firebug.getRepObject(target); 1681 1682 if (hasClass(target, "cssPropName")) 1683 { 1684 if (value && previousValue != value) // name of property has changed. 1685 { 1686 var propValue = getChildByClass(row, "cssPropValue").textContent; 1687 var parsedValue = parsePriority(propValue); 1688 1689 if (propValue && propValue != "undefined") { 1690 if (FBTrace.DBG_CSS) 1691 FBTrace.sysout("CSSEditor.saveEdit : "+previousValue+"->"+value+" = "+propValue+"\n"); 1692 if (previousValue) 1693 Firebug.CSSModule.removeProperty(rule, previousValue); 1694 Firebug.CSSModule.setProperty(rule, value, parsedValue.value, parsedValue.priority); 1695 } 1696 } 1697 else if (!value) // name of the property has been deleted, so remove the property. 1698 Firebug.CSSModule.removeProperty(rule, previousValue); 1699 } 1700 else if (getAncestorByClass(target, "cssPropValue")) 1701 { 1702 var propName = getChildByClass(row, "cssPropName").textContent; 1703 var propValue = getChildByClass(row, "cssPropValue").textContent; 1704 1705 if (FBTrace.DBG_CSS) 1706 { 1707 FBTrace.sysout("CSSEditor.saveEdit propName=propValue: "+propName +" = "+propValue+"\n"); 1708 // FBTrace.sysout("CSSEditor.saveEdit BEFORE style:",style); 1709 } 1710 1711 if (value && value != "null") 1712 { 1713 var parsedValue = parsePriority(value); 1714 Firebug.CSSModule.setProperty(rule, propName, parsedValue.value, parsedValue.priority); 1715 } 1716 else if (previousValue && previousValue != "null") 1717 Firebug.CSSModule.removeProperty(rule, propName); 1718 } 1719 1720 this.panel.markChange(this.panel.name == "stylesheet"); 1721 }, 1722 1723 advanceToNext: function(target, charCode) 1724 { 1725 if (charCode == 58 /*":"*/ && hasClass(target, "cssPropName")) 1726 return true; 1727 }, 1728 1729 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1730 1731 getAutoCompleteRange: function(value, offset) 1732 { 1733 if (hasClass(this.target, "cssPropName")) 1734 return {start: 0, end: value.length-1}; 1735 else 1736 return parseCSSValue(value, offset); 1737 }, 1738 1739 getAutoCompleteList: function(preExpr, expr, postExpr) 1740 { 1741 if (hasClass(this.target, "cssPropName")) 1742 { 1743 return getCSSPropertyNames(); 1744 } 1745 else 1746 { 1747 var row = getAncestorByClass(this.target, "cssProp"); 1748 var propName = getChildByClass(row, "cssPropName").textContent; 1749 return getCSSKeywordsByProperty(propName); 1750 } 1751 } 1752 }); 1753 1754 //************************************************************************************************ 1755 //CSSRuleEditor 1756 1757 function CSSRuleEditor(doc) 1758 { 1759 this.initializeInline(doc); 1760 this.completeAsYouType = false; 1761 } 1762 CSSRuleEditor.uniquifier = 0; 1763 CSSRuleEditor.prototype = domplate(Firebug.InlineEditor.prototype, 1764 { 1765 insertNewRow: function(target, insertWhere) 1766 { 1767 var emptyRule = { 1768 selector: "", 1769 id: "", 1770 props: [], 1771 isSelectorEditable: true 1772 }; 1773 1774 if (insertWhere == "before") 1775 return CSSStyleRuleTag.tag.insertBefore({rule: emptyRule}, target); 1776 else 1777 return CSSStyleRuleTag.tag.insertAfter({rule: emptyRule}, target); 1778 }, 1779 1780 saveEdit: function(target, value, previousValue) 1781 { 1782 if (FBTrace.DBG_CSS) 1783 FBTrace.sysout("CSSRuleEditor.saveEdit: '" + value + "' '" + previousValue + "'", target); 1784 1785 target.innerHTML = escapeForCss(value); 1786 1787 if (value === previousValue) return; 1788 1789 var row = getAncestorByClass(target, "cssRule"); 1790 var styleSheet = this.panel.location; 1791 styleSheet = styleSheet.editStyleSheet ? styleSheet.editStyleSheet.sheet : styleSheet; 1792 1793 var cssRules = styleSheet.cssRules; 1794 var rule = Firebug.getRepObject(target), oldRule = rule; 1795 var ruleIndex = cssRules.length; 1796 if (rule || Firebug.getRepObject(row.nextSibling)) 1797 { 1798 var searchRule = rule || Firebug.getRepObject(row.nextSibling); 1799 for (ruleIndex=0; ruleIndex<cssRules.length && searchRule!=cssRules[ruleIndex]; ruleIndex++) {} 1800 } 1801 1802 // Delete in all cases except for new add 1803 // We want to do this before the insert to ease change tracking 1804 if (oldRule) 1805 { 1806 Firebug.CSSModule.deleteRule(styleSheet, ruleIndex); 1807 } 1808 1809 // Firefox does not follow the spec for the update selector text case. 1810 // When attempting to update the value, firefox will silently fail. 1811 // See https://bugzilla.mozilla.org/show_bug.cgi?id=37468 for the quite 1812 // old discussion of this bug. 1813 // As a result we need to recreate the style every time the selector 1814 // changes. 1815 if (value) 1816 { 1817 var cssText = [ value, "{", ]; 1818 var props = row.getElementsByClassName("cssProp"); 1819 for (var i = 0; i < props.length; i++) { 1820 var propEl = props[i]; 1821 if (!hasClass(propEl, "disabledStyle")) { 1822 cssText.push(getChildByClass(propEl, "cssPropName").textContent); 1823 cssText.push(":"); 1824 cssText.push(getChildByClass(propEl, "cssPropValue").textContent); 1825 cssText.push(";"); 1826 } 1827 } 1828 cssText.push("}"); 1829 cssText = cssText.join(""); 1830 1831 try 1832 { 1833 var insertLoc = Firebug.CSSModule.insertRule(styleSheet, cssText, ruleIndex); 1834 rule = cssRules[insertLoc]; 1835 ruleIndex++; 1836 } 1837 catch (err) 1838 { 1839 if (FBTrace.DBG_CSS || FBTrace.DBG_ERRORS) 1840 FBTrace.sysout("CSS Insert Error: "+err, err); 1841 1842 target.innerHTML = escapeForCss(previousValue); 1843 row.repObject = undefined; 1844 return; 1845 } 1846 } else { 1847 rule = undefined; 1848 } 1849 1850 // Update the rep object 1851 row.repObject = rule; 1852 if (!oldRule) 1853 { 1854 // Who knows what the domutils will return for rule line 1855 // for a recently created rule. To be safe we just generate 1856 // a unique value as this is only used as an internal key. 1857 var ruleId = "new/"+value+"/"+(++CSSRuleEditor.uniquifier); 1858 row.setAttribute("ruleId", ruleId); 1859 } 1860 1861 this.panel.markChange(this.panel.name == "stylesheet"); 1862 } 1863 }); 1864 1865 // ************************************************************************************************ 1866 // StyleSheetEditor 1867 1868 function StyleSheetEditor(doc) 1869 { 1870 this.box = this.tag.replace({}, doc, this); 1871 this.input = this.box.firstChild; 1872 } 1873 1874 StyleSheetEditor.prototype = domplate(Firebug.BaseEditor, 1875 { 1876 multiLine: true, 1877 1878 tag: DIV( 1879 TEXTAREA({class: "styleSheetEditor fullPanelEditor", oninput: "$onInput"}) 1880 ), 1881 1882 getValue: function() 1883 { 1884 return this.input.value; 1885 }, 1886 1887 setValue: function(value) 1888 { 1889 return this.input.value = value; 1890 }, 1891 1892 show: function(target, panel, value, textSize, targetSize) 1893 { 1894 this.target = target; 1895 this.panel = panel; 1896 1897 this.panel.panelNode.appendChild(this.box); 1898 1899 this.input.value = value; 1900 this.input.focus(); 1901 1902 var command = Firebug.chrome.$("cmd_toggleCSSEditing"); 1903 command.setAttribute("checked", true); 1904 }, 1905 1906 hide: function() 1907 { 1908 var command = Firebug.chrome.$("cmd_toggleCSSEditing"); 1909 command.setAttribute("checked", false); 1910 1911 if (this.box.parentNode == this.panel.panelNode) 1912 this.panel.panelNode.removeChild(this.box); 1913 1914 delete this.target; 1915 delete this.panel; 1916 delete this.styleSheet; 1917 }, 1918 1919 saveEdit: function(target, value, previousValue) 1920 { 1921 Firebug.CSSModule.freeEdit(this.styleSheet, value); 1922 }, 1923 1924 beginEditing: function() 1925 { 1926 this.editing = true; 1927 }, 1928 1929 endEditing: function() 1930 { 1931 this.editing = false; 1932 this.panel.refresh(); 1933 return true; 1934 }, 1935 1936 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1937 1938 onInput: function() 1939 { 1940 Firebug.Editor.update(); 1941 }, 1942 1943 scrollToLine: function(line, offset) 1944 { 1945 this.startMeasuring(this.input); 1946 var lineHeight = this.measureText().height; 1947 this.stopMeasuring(); 1948 1949 this.input.scrollTop = (line * lineHeight) + offset; 1950 } 1951 }); 1952 1953 // ************************************************************************************************ 1954 // Local Helpers 1955 1956 function rgbToHex(value) 1957 { 1958 return value.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) { 1959 return '#' + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase(); 1960 }); 1961 } 1962 1963 function stripUnits(value) 1964 { 1965 // remove units from '0px', '0em' etc. leave non-zero units in-tact. 1966 return value.replace(/(url\(.*?\)|[^0]\S*\s*)|0(%|em|ex|px|in|cm|mm|pt|pc)(\s|$)/gi, function(_, skip, remove, whitespace) { 1967 return skip || ('0' + whitespace); 1968 }); 1969 } 1970 1971 function parsePriority(value) 1972 { 1973 var rePriority = /(.*?)\s*(!important)?$/; 1974 var m = rePriority.exec(value); 1975 var propValue = m ? m[1] : ""; 1976 var priority = m && m[2] ? "important" : ""; 1977 return {value: propValue, priority: priority}; 1978 } 1979 1980 function parseURLValue(value) 1981 { 1982 var m = reURL.exec(value); 1983 return m ? m[1] : ""; 1984 } 1985 1986 function parseRepeatValue(value) 1987 { 1988 var m = reRepeat.exec(value); 1989 return m ? m[0] : ""; 1990 } 1991 1992 function parseCSSValue(value, offset) 1993 { 1994 var start = 0; 1995 var m; 1996 while (1) 1997 { 1998 m = reSplitCSS.exec(value); 1999 if (m && m.index+m[0].length < offset) 2000 { 2001 value = value.substr(m.index+m[0].length); 2002 start += m.index+m[0].length; 2003 offset -= m.index+m[0].length; 2004 } 2005 else 2006 break; 2007 } 2008 2009 if (m) 2010 { 2011 var type; 2012 if (m[1]) 2013 type = "url"; 2014 else if (m[2] || m[3]) 2015 type = "rgb"; 2016 else if (m[4]) 2017 type = "int"; 2018 2019 return {value: m[0], start: start+m.index, end: start+m.index+(m[0].length-1), type: type}; 2020 } 2021 } 2022 2023 function findPropByName(props, name) 2024 { 2025 for (var i = 0; i < props.length; ++i) 2026 { 2027 if (props[i].name == name) 2028 return i; 2029 } 2030 } 2031 2032 function sortProperties(props) 2033 { 2034 props.sort(function(a, b) 2035 { 2036 return a.name > b.name ? 1 : -1; 2037 }); 2038 } 2039 2040 function getTopmostRuleLine(panelNode) 2041 { 2042 for (var child = panelNode.firstChild; child; child = child.nextSibling) 2043 { 2044 if (child.offsetTop+child.offsetHeight > panelNode.scrollTop) 2045 { 2046 var rule = child.repObject; 2047 if (rule) 2048 return { 2049 line: domUtils.getRuleLine(rule), 2050 offset: panelNode.scrollTop-child.offsetTop 2051 }; 2052 } 2053 } 2054 return 0; 2055 } 2056 2057 function getStyleSheetCSS(sheet, context) 2058 { 2059 if (sheet.ownerNode instanceof HTMLStyleElement) 2060 return sheet.ownerNode.innerHTML; 2061 else 2062 return context.sourceCache.load(sheet.href).join(""); 2063 } 2064 2065 function getStyleSheetOwnerNode(sheet) { 2066 for (; sheet && !sheet.ownerNode; sheet = sheet.parentStyleSheet); 2067 2068 return sheet.ownerNode; 2069 } 2070 2071 function scrollSelectionIntoView(panel) 2072 { 2073 var selCon = getSelectionController(panel); 2074 selCon.scrollSelectionIntoView( 2075 nsISelectionController.SELECTION_NORMAL, 2076 nsISelectionController.SELECTION_FOCUS_REGION, true); 2077 } 2078 2079 function getSelectionController(panel) 2080 { 2081 var browser = Firebug.chrome.getPanelBrowser(panel); 2082 return browser.docShell.QueryInterface(nsIInterfaceRequestor<