1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/lib/object", 5 "firebug/firebug", 6 "firebug/lib/domplate", 7 "firebug/chrome/reps", 8 "firebug/lib/locale", 9 "firebug/lib/events", 10 "firebug/lib/url", 11 "firebug/js/sourceLink", 12 "firebug/lib/css", 13 "firebug/lib/dom", 14 "firebug/chrome/window", 15 "firebug/lib/search", 16 "firebug/lib/string", 17 "firebug/lib/array", 18 "firebug/lib/fonts", 19 "firebug/lib/xml", 20 "firebug/lib/persist", 21 "firebug/lib/system", 22 "firebug/chrome/menu", 23 "firebug/lib/options", 24 "firebug/css/cssModule", 25 "firebug/css/cssReps", 26 "firebug/css/selectorEditor", 27 "firebug/editor/editor", 28 "firebug/editor/editorSelector", 29 "firebug/chrome/searchBox" 30 ], 31 function(Obj, Firebug, Domplate, FirebugReps, Locale, Events, Url, SourceLink, Css, Dom, Win, 32 Search, Str, Arr, Fonts, Xml, Persist, System, Menu, Options, CSSModule, CSSInfoTip, 33 SelectorEditor) { 34 35 with (Domplate) { 36 37 // ********************************************************************************************* // 38 // Constants 39 40 const Cc = Components.classes; 41 const Ci = Components.interfaces; 42 43 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 44 45 var CSSDomplateBase = 46 { 47 isEditable: function(rule) 48 { 49 return !rule.isSystemSheet && !rule.isNotEditable; 50 }, 51 52 isSelectorEditable: function(rule) 53 { 54 return rule.isSelectorEditable && this.isEditable(rule); 55 }, 56 57 getPropertyValue: function(prop) 58 { 59 // Disabled, see http://code.google.com/p/fbug/issues/detail?id=5880 60 /* 61 var limit = Options.get("stringCropLength"); 62 */ 63 var limit = 0; 64 if (limit > 0) 65 return Str.cropString(prop.value, limit); 66 return prop.value; 67 } 68 }; 69 70 var CSSPropTag = domplate(CSSDomplateBase, 71 { 72 tag: 73 DIV({"class": "cssProp focusRow", $disabledStyle: "$prop.disabled", 74 $editGroup: "$rule|isEditable", 75 $cssOverridden: "$prop.overridden", 76 role: "option"}, 77 78 // Use spaces for indent to make "copy to clipboard" nice. 79 SPAN(" "), 80 SPAN({"class": "cssPropName", $editable: "$rule|isEditable"}, 81 "$prop.name" 82 ), 83 84 // Use a space here, so that "copy to clipboard" has it (issue 3266). 85 SPAN({"class": "cssColon"}, ": "), 86 SPAN({"class": "cssPropValue", $editable: "$rule|isEditable"}, 87 "$prop|getPropertyValue$prop.important" 88 ), 89 SPAN({"class": "cssSemi"}, ";") 90 ) 91 }); 92 93 var CSSRuleTag = 94 TAG("$rule.tag", {rule: "$rule"}); 95 96 var CSSImportRuleTag = domplate(CSSDomplateBase, 97 { 98 tag: 99 DIV({"class": "cssRule insertInto focusRow importRule", _repObject: "$rule.rule"}, 100 "@import "", 101 A({"class": "objectLink", _repObject: "$rule.rule.styleSheet"}, "$rule.rule.href"), 102 """, 103 SPAN({"class": "separator"}, "$rule.rule|getSeparator"), 104 SPAN({"class": "cssMediaQuery", $editable: "$rule|isEditable"}, 105 "$rule.rule.media.mediaText"), 106 ";" 107 ), 108 109 getSeparator: function(rule) 110 { 111 return rule.media.mediaText == "" ? "" : " "; 112 } 113 }); 114 115 var CSSCharsetRuleTag = domplate(CSSDomplateBase, 116 { 117 tag: 118 DIV({"class": "cssRule focusRow cssCharsetRule", _repObject: "$rule.rule"}, 119 SPAN({"class": "cssRuleName"}, "@charset"), 120 " "", 121 SPAN({"class": "cssRuleValue", $editable: "$rule|isEditable"}, "$rule.rule.encoding"), 122 "";" 123 ) 124 }); 125 126 var CSSNamespaceRuleTag = domplate(CSSDomplateBase, 127 { 128 tag: 129 DIV({"class": "cssRule focusRow cssNamespaceRule", _repObject: "$rule.rule"}, 130 SPAN({"class": "cssRuleName"}, "@namespace"), 131 SPAN({"class": "separator"}, "$rule.prefix|getSeparator"), 132 SPAN({"class": "cssNamespacePrefix", $editable: "$rule|isEditable"}, "$rule.prefix"), 133 " "", 134 SPAN({"class": "cssNamespaceName", $editable: "$rule|isEditable"}, "$rule.name"), 135 "";" 136 ), 137 138 getSeparator: function(prefix) 139 { 140 return prefix == "" ? "" : " "; 141 } 142 }); 143 144 var CSSFontFaceRuleTag = domplate(CSSDomplateBase, 145 { 146 tag: 147 DIV({"class": "cssRule cssFontFaceRule", 148 $cssEditableRule: "$rule|isEditable", 149 $insertInto: "$rule|isEditable", 150 _repObject: "$rule.rule", 151 role : 'presentation'}, 152 DIV({"class": "cssHead focusRow", role : "listitem"}, "@font-face {"), 153 DIV({role : "group"}, 154 DIV({"class": "cssPropertyListBox", role: "listbox"}, 155 FOR("prop", "$rule.props", 156 TAG(CSSPropTag.tag, {rule: "$rule", prop: "$prop"}) 157 ) 158 ) 159 ), 160 DIV({$editable: "$rule|isEditable", $insertBefore:"$rule|isEditable", 161 role:"presentation"}, 162 "}" 163 ) 164 ) 165 }); 166 167 var CSSStyleRuleTag = domplate(CSSDomplateBase, 168 { 169 tag: 170 DIV({"class": "cssRule", 171 $cssEditableRule: "$rule|isEditable", 172 $insertInto: "$rule|isEditable", 173 $editGroup: "$rule|isSelectorEditable", 174 _repObject: "$rule.rule", 175 role: "presentation"}, 176 DIV({"class": "cssHead focusRow", role: "listitem"}, 177 SPAN({"class": "cssSelector", $editable: "$rule|isSelectorEditable"}, 178 "$rule.selector"), 179 " {" 180 ), 181 DIV({role: "group"}, 182 DIV({"class": "cssPropertyListBox", _rule: "$rule", role: "listbox"}, 183 FOR("prop", "$rule.props", 184 TAG(CSSPropTag.tag, {rule: "$rule", prop: "$prop"}) 185 ) 186 ) 187 ), 188 DIV({$editable: "$rule|isEditable", $insertBefore: "$rule|isEditable", 189 role:"presentation"}, 190 "}" 191 ) 192 ) 193 }); 194 195 Firebug.CSSStyleRuleTag = CSSStyleRuleTag; 196 197 // ********************************************************************************************* // 198 199 const reSplitCSS = /(url\("?[^"\)]+?"?\))|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/; 200 const reURL = /url\("?([^"\)]+)?"?\)/; 201 const reRepeat = /no-repeat|repeat-x|repeat-y|repeat/; 202 203 // ********************************************************************************************* // 204 // CSS Module 205 206 Firebug.CSSStyleSheetPanel = function() {}; 207 208 Firebug.CSSStyleSheetPanel.prototype = Obj.extend(Firebug.Panel, 209 { 210 template: domplate( 211 { 212 tag: 213 DIV({"class": "cssSheet insertInto a11yCSSView"}, 214 FOR("rule", "$rules", 215 CSSRuleTag 216 ), 217 DIV({"class": "cssSheet editable insertBefore"}, "" 218 ) 219 ) 220 }), 221 222 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 223 224 refresh: function() 225 { 226 if (this.location) 227 this.updateLocation(this.location); 228 else if (this.selection) 229 this.updateSelection(this.selection); 230 }, 231 232 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 233 // CSS Editing 234 235 startBuiltInEditing: function(css) 236 { 237 if (FBTrace.DBG_CSS) 238 FBTrace.sysout("CSSStyleSheetPanel.startBuiltInEditing", css); 239 240 if (!this.stylesheetEditor) 241 this.stylesheetEditor = new StyleSheetEditor(this.document); 242 243 var styleSheet = this.location.editStyleSheet 244 ? this.location.editStyleSheet.sheet 245 : this.location; 246 247 this.stylesheetEditor.styleSheet = this.location; 248 Firebug.Editor.startEditing(this.panelNode, css, this.stylesheetEditor); 249 250 //this.stylesheetEditor.scrollToLine(topmost.line, topmost.offset); 251 this.stylesheetEditor.input.scrollTop = this.panelNode.scrollTop; 252 }, 253 254 startLiveEditing: function(styleSheet, context) 255 { 256 var css = getStyleSheetCSS(styleSheet, context); 257 this.startBuiltInEditing(css); 258 }, 259 260 startSourceEditing: function(styleSheet, context) 261 { 262 if (Firebug.CSSDirtyListener.isDirty(styleSheet, context)) 263 { 264 var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]. 265 getService(Ci.nsIPromptService); 266 267 var proceedToEdit = prompts.confirm(null, Locale.$STR("Firebug"), 268 Locale.$STR("confirmation.Edit_CSS_Source")); 269 270 if (!proceedToEdit) 271 { 272 this.stopEditing(); 273 return; 274 } 275 } 276 277 var css = getOriginalStyleSheetCSS(styleSheet, context); 278 this.startBuiltInEditing(css); 279 }, 280 281 stopEditing: function() 282 { 283 if (FBTrace.DBG_CSS) 284 FBTrace.sysout("CSSStyleSheetPanel.stopEditing"); 285 286 if (this.currentCSSEditor) 287 { 288 this.currentCSSEditor.stopEditing(); 289 delete this.currentCSSEditor; 290 } 291 else 292 { 293 Firebug.Editor.stopEditing(); 294 } 295 }, 296 297 toggleEditing: function() 298 { 299 if (this.editing) 300 { 301 this.stopEditing(); 302 Events.dispatch(this.fbListeners, "onStopCSSEditing", [this.context]); 303 } 304 else 305 { 306 if (!this.location) 307 return; 308 309 var styleSheet = this.location.editStyleSheet 310 ? this.location.editStyleSheet.sheet 311 : this.location; 312 313 this.currentCSSEditor = CSSModule.getCurrentEditor(); 314 try 315 { 316 this.currentCSSEditor.startEditing(styleSheet, this.context, this); 317 Events.dispatch(this.fbListeners, "onStartCSSEditing", [styleSheet, this.context]); 318 } 319 catch(exc) 320 { 321 var mode = CSSModule.getCurrentEditorName(); 322 if (FBTrace.DBG_ERRORS) 323 FBTrace.sysout("editor.startEditing ERROR "+exc, {exc: exc, name: mode, 324 currentEditor: this.currentCSSEditor, styleSheet: styleSheet, 325 CSSModule: CSSModule}); 326 } 327 } 328 }, 329 330 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 331 332 loadOriginalSource: function() 333 { 334 if (!this.location) 335 return; 336 337 var styleSheet = this.location; 338 339 var css = getOriginalStyleSheetCSS(styleSheet, this.context); 340 341 this.stylesheetEditor.setValue(css); 342 this.stylesheetEditor.saveEdit(null, css); 343 //styleSheet.editStyleSheet.showUnformated = true; 344 }, 345 346 getStylesheetURL: function(rule, getBaseUri) 347 { 348 if (this.location.href) 349 return this.location.href; 350 else if (getBaseUri) 351 return this.context.window.document.baseURI; 352 else 353 return this.context.window.location.href; 354 }, 355 356 getRuleByLine: function(styleSheet, line) 357 { 358 if (!Dom.domUtils) 359 return null; 360 361 var cssRules = styleSheet.cssRules; 362 for (var i = 0; i < cssRules.length; ++i) 363 { 364 var rule = cssRules[i]; 365 var previousRule; 366 if (rule instanceof window.CSSStyleRule) 367 { 368 var selectorLine = Dom.domUtils.getRuleLine(rule); 369 // The declarations are on lines equal or greater than the selectorLine 370 if (selectorLine === line) // then the line requested is a selector line 371 return rule; 372 if (selectorLine > line) // then we passed the rule for the requested line 373 return previousRule; 374 // else the requested line is still ahead 375 previousRule = rule; 376 } 377 } 378 }, 379 380 highlightRule: function(rule) 381 { 382 var ruleElement = Firebug.getElementByRepObject(this.panelNode.firstChild, rule); 383 if (ruleElement) 384 { 385 Dom.scrollIntoCenterView(ruleElement, this.panelNode); 386 Css.setClassTimed(ruleElement, "jumpHighlight", this.context); 387 } 388 }, 389 390 getStyleSheetRules: function(context, styleSheet) 391 { 392 if (!styleSheet) 393 return []; 394 395 var isSystemSheet = Url.isSystemStyleSheet(styleSheet); 396 397 var rules = []; 398 var appendRules = function(cssRules) 399 { 400 var i, props; 401 402 if (!cssRules) 403 return; 404 405 for (i=0; i<cssRules.length; ++i) 406 { 407 var rule = cssRules[i]; 408 if (rule instanceof window.CSSStyleRule) 409 { 410 props = this.getRuleProperties(context, rule); 411 rules.push({ 412 tag: CSSStyleRuleTag.tag, 413 rule: rule, 414 selector: rule.selectorText.replace(/ :/g, " *:"), // (issue 3683) 415 props: props, 416 isSystemSheet: isSystemSheet, 417 isSelectorEditable: true 418 }); 419 } 420 else if (rule instanceof window.CSSImportRule) 421 { 422 rules.push({tag: CSSImportRuleTag.tag, rule: rule}); 423 } 424 else if (rule instanceof window.CSSCharsetRule) 425 { 426 rules.push({tag: CSSCharsetRuleTag.tag, rule: rule}); 427 } 428 else if (rule instanceof window.CSSMediaRule || 429 rule instanceof window.CSSMozDocumentRule) 430 { 431 appendRules(Css.safeGetCSSRules(rule)); 432 } 433 else if (rule instanceof window.CSSFontFaceRule) 434 { 435 props = this.parseCSSProps(rule.style); 436 this.sortProperties(props); 437 rules.push({ 438 tag: CSSFontFaceRuleTag.tag, rule: rule, 439 props: props, isSystemSheet: isSystemSheet, 440 isNotEditable: true 441 }); 442 } 443 else if (rule instanceof window.CSSNameSpaceRule && 444 !(rule instanceof window.MozCSSKeyframesRule || 445 rule instanceof window.MozCSSKeyframeRule)) 446 { 447 // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=754772 448 // MozCSSKeyframesRules and MozCSSKeyframeRules are recognized as 449 // CSSNameSpaceRules, so explicitly check whether the rule is not a 450 // MozCSSKeyframesRule or a MozCSSKeyframeRule 451 452 var reNamespace = /^@namespace ((.+) )?url\("(.*?)"\);$/; 453 var namespace = rule.cssText.match(reNamespace); 454 var prefix = namespace[2] || ""; 455 var name = namespace[3]; 456 rules.push({tag: CSSNamespaceRuleTag.tag, rule: rule, prefix: prefix, 457 name: name, isNotEditable: true}); 458 } 459 else 460 { 461 if (FBTrace.DBG_ERRORS && FBTrace.DBG_CSS) 462 FBTrace.sysout("css getStyleSheetRules failed to classify a rule ", rule); 463 } 464 } 465 }.bind(this); 466 467 appendRules(Css.safeGetCSSRules(styleSheet)); 468 return rules; 469 }, 470 471 parseCSSProps: function(style, inheritMode) 472 { 473 var m; 474 var props = []; 475 476 if (Firebug.expandShorthandProps) 477 { 478 var count = style.length-1; 479 var index = style.length; 480 481 while (index--) 482 { 483 var propName = style.item(count - index); 484 this.addProperty(propName, style.getPropertyValue(propName), 485 !!style.getPropertyPriority(propName), false, inheritMode, props); 486 } 487 } 488 else 489 { 490 var lines = style.cssText.match(/(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g); 491 var propRE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(! important)?;?$/; 492 var line; 493 var i=0; 494 while(line = lines[i++]) 495 { 496 m = propRE.exec(line); 497 if(!m) 498 continue; 499 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 509 sortProperties: function(props) 510 { 511 props.sort(function(a, b) 512 { 513 return a.name > b.name ? 1 : -1; 514 }); 515 }, 516 517 getRuleProperties: function(context, rule, inheritMode) 518 { 519 var props = this.parseCSSProps(rule.style, inheritMode); 520 521 this.addDisabledProperties(context, rule, inheritMode, props); 522 this.sortProperties(props); 523 524 return props; 525 }, 526 527 addDisabledProperties: function(context, rule, inheritMode, props) 528 { 529 var disabledMap = this.getDisabledMap(context); 530 var moreProps = disabledMap.get(rule); 531 if (moreProps) 532 { 533 var propMap = {}; 534 for (var i = 0; i < props.length; ++i) 535 propMap[props[i].name] = true; 536 537 for (var i = 0; i < moreProps.length; ++i) 538 { 539 var prop = moreProps[i]; 540 if (propMap.hasOwnProperty(prop.name)) 541 { 542 // A (probably enabled) property with the same name as this 543 // disabled one has appeared - remove this one entirely. 544 moreProps.splice(i, 1); 545 --i; 546 continue; 547 } 548 propMap[prop.name] = true; 549 this.addProperty(prop.name, prop.value, prop.important, true, inheritMode, props); 550 } 551 } 552 }, 553 554 addProperty: function(name, value, important, disabled, inheritMode, props) 555 { 556 if (inheritMode && !Dom.domUtils.isInheritedProperty(name)) 557 return; 558 559 name = this.translateName(name, value); 560 if (name) 561 { 562 value = Css.stripUnits(formatColor(value)); 563 important = important ? " !important" : ""; 564 565 var prop = {name: name, value: value, important: important, disabled: disabled}; 566 props.push(prop); 567 } 568 }, 569 570 translateName: function(name, value) 571 { 572 // Don't show these proprietary Mozilla properties 573 if ((value == "-moz-initial" 574 && (name == "-moz-background-clip" || name == "-moz-background-origin" 575 || name == "-moz-background-inline-policy")) 576 || (value == "physical" 577 && (name == "margin-left-ltr-source" || name == "margin-left-rtl-source" 578 || name == "margin-right-ltr-source" || name == "margin-right-rtl-source")) 579 || (value == "physical" 580 && (name == "padding-left-ltr-source" || name == "padding-left-rtl-source" 581 || name == "padding-right-ltr-source" || name == "padding-right-rtl-source"))) 582 return null; 583 584 // Translate these back to the form the user probably expects 585 if (name == "margin-left-value") 586 return "margin-left"; 587 else if (name == "margin-right-value") 588 return "margin-right"; 589 else if (name == "margin-top-value") 590 return "margin-top"; 591 else if (name == "margin-bottom-value") 592 return "margin-bottom"; 593 else if (name == "padding-left-value") 594 return "padding-left"; 595 else if (name == "padding-right-value") 596 return "padding-right"; 597 else if (name == "padding-top-value") 598 return "padding-top"; 599 else if (name == "padding-bottom-value") 600 return "padding-bottom"; 601 // XXXjoe What about border! 602 else 603 return name; 604 }, 605 606 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 607 608 getDisabledMap: function(context) 609 { 610 // Ideally, we'd use a WeakMap here, but WeakMaps don't allow CSS rules 611 // as keys before Firefox 17. A Map is used instead. (cf. bug 777373.) 612 if (!context.cssDisabledMap) 613 context.cssDisabledMap = new Map(); 614 return context.cssDisabledMap; 615 }, 616 617 remapRule: function(context, oldRule, newRule) 618 { 619 var map = this.getDisabledMap(context); 620 if (map.has(oldRule)) 621 map.set(newRule, map.get(oldRule)); 622 }, 623 624 editElementStyle: function() 625 { 626 var rulesBox = this.panelNode.getElementsByClassName("cssElementRuleContainer")[0]; 627 var styleRuleBox = rulesBox && Firebug.getElementByRepObject(rulesBox, this.selection); 628 if (!styleRuleBox) 629 { 630 var rule = {rule: this.selection, inherited: false, selector: "element.style", props: []}; 631 if (!rulesBox) 632 { 633 // The element did not have any displayed styles. We need to create the 634 // whole tree and remove the no styles message 635 styleRuleBox = this.template.cascadedTag.replace({ 636 rules: [rule], inherited: [], inheritLabel: Locale.$STR("InheritedFrom") 637 }, this.panelNode); 638 639 styleRuleBox = styleRuleBox.getElementsByClassName("cssElementRuleContainer")[0]; 640 } 641 else 642 styleRuleBox = this.template.ruleTag.insertBefore({rule: rule}, rulesBox); 643 644 styleRuleBox = styleRuleBox.getElementsByClassName("insertInto")[0]; 645 } 646 647 Firebug.Editor.insertRowForObject(styleRuleBox); 648 }, 649 650 addRelatedRule: function() 651 { 652 if (!this.panelNode.getElementsByClassName("cssElementRuleContainer")[0]) 653 { 654 // The element did not have any displayed styles - create the whole 655 // tree and remove the no styles message. 656 this.template.cascadedTag.replace({ 657 rules: [], inherited: [], 658 inheritLabel: Locale.$STR("InheritedFrom") 659 }, this.panelNode); 660 } 661 662 // Insert the new rule at the top, or after the style rules if there 663 // are any. 664 var container = this.panelNode.getElementsByClassName("cssNonInherited")[0]; 665 var ruleBox = container.getElementsByClassName("cssElementRuleContainer")[0]; 666 var styleRuleBox = ruleBox && Firebug.getElementByRepObject(ruleBox, this.selection); 667 if (styleRuleBox) 668 ruleBox = this.template.newRuleTag.insertAfter({}, ruleBox); 669 else if (ruleBox) 670 ruleBox = this.template.newRuleTag.insertBefore({}, ruleBox); 671 else 672 ruleBox = this.template.newRuleTag.append({}, container); 673 674 var before = ruleBox.getElementsByClassName("insertBefore")[0]; 675 Firebug.Editor.insertRow(before, "before"); 676 677 // Auto-fill the selector field with something reasonable, like 678 // ".some-class" or "#table td". 679 var el = this.selection, doc = el.ownerDocument; 680 var base = Xml.getNodeName(el), autofill; 681 if (el.className) 682 { 683 autofill = "." + Arr.cloneArray(el.classList).join("."); 684 } 685 else 686 { 687 var level = 0; 688 el = el.parentNode; 689 while (!autofill && el !== doc) 690 { 691 ++level; 692 if (el.id !== "") 693 autofill = "#" + el.id; 694 else if (el.className !== "") 695 autofill = "." + Arr.cloneArray(el.classList).join("."); 696 el = el.parentNode; 697 } 698 if (autofill) 699 { 700 if (level === 1) 701 autofill += " >"; 702 autofill += " " + base; 703 } 704 } 705 if (!autofill || 706 doc.querySelectorAll(autofill).length === doc.querySelectorAll(base).length) 707 { 708 autofill = base; 709 } 710 this.ruleEditor.setValue(autofill); 711 this.ruleEditor.input.select(); 712 Firebug.Editor.update(true); 713 }, 714 715 editMediaQuery: function(target) 716 { 717 var row = Dom.getAncestorByClass(target, "cssRule"); 718 var mediaQueryBox = Dom.getChildByClass(row, "cssMediaQuery"); 719 Firebug.Editor.startEditing(mediaQueryBox); 720 }, 721 722 insertPropertyRow: function(row) 723 { 724 Firebug.Editor.insertRowForObject(row); 725 }, 726 727 insertRule: function(row) 728 { 729 var location = Dom.getAncestorByClass(row, "cssRule"); 730 if (!location) 731 { 732 location = Dom.getChildByClass(this.panelNode, "cssSheet"); 733 734 // Stylesheet has no rules 735 if (!location) 736 this.template.tag.replace({rules: []}, this.panelNode); 737 738 location = Dom.getChildByClass(this.panelNode, "cssSheet"); 739 Firebug.Editor.insertRowForObject(location); 740 } 741 else 742 { 743 Firebug.Editor.insertRow(location, "before"); 744 } 745 }, 746 747 editPropertyRow: function(row) 748 { 749 var propValueBox = Dom.getChildByClass(row, "cssPropValue"); 750 Firebug.Editor.startEditing(propValueBox); 751 }, 752 753 deletePropertyRow: function(row) 754 { 755 var rule = Firebug.getRepObject(row); 756 var propName = Dom.getChildByClass(row, "cssPropName").textContent; 757 758 // Try removing the property from the "disabled" map. 759 var wasDisabled = this.removeDisabledProperty(rule, propName); 760 761 // If that fails, remove the actual property instead. 762 if (!wasDisabled) 763 CSSModule.deleteProperty(rule, propName, this.context); 764 765 if (this.name == "stylesheet") 766 Events.dispatch(this.fbListeners, "onInlineEditorClose", [this, row.firstChild, true]); 767 row.parentNode.removeChild(row); 768 769 this.markChange(this.name == "stylesheet"); 770 }, 771 772 removeDisabledProperty: function(rule, propName) 773 { 774 var disabledMap = this.getDisabledMap(this.context); 775 var map = disabledMap.get(rule); 776 if (!map) 777 return false; 778 for (var i = 0; i < map.length; ++i) 779 { 780 if (map[i].name === propName) 781 { 782 map.splice(i, 1); 783 return true; 784 } 785 } 786 return false; 787 }, 788 789 disablePropertyRow: function(row) 790 { 791 Css.toggleClass(row, "disabledStyle"); 792 793 var rule = Firebug.getRepObject(row); 794 var propName = Dom.getChildByClass(row, "cssPropName").textContent; 795 796 var disabledMap = this.getDisabledMap(this.context); 797 if (!disabledMap.has(rule)) 798 disabledMap.set(rule, []); 799 var map = disabledMap.get(rule); 800 801 var propValue = Dom.getChildByClass(row, "cssPropValue").textContent; 802 var parsedValue = parsePriority(propValue); 803 804 CSSModule.disableProperty(Css.hasClass(row, "disabledStyle"), rule, 805 propName, parsedValue, map, this.context); 806 807 this.markChange(this.name == "stylesheet"); 808 }, 809 810 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 811 812 // When handling disable button clicks, we cannot simply use a 'click' 813 // event, because refresh() may be (and often is) called in between 814 // mousedown and mouseup, replacing the DOM structure. Instead, a 815 // description of the moused-down disable button's property is saved 816 // and explicitly checked on mouseup (issue 5500). 817 clickedPropTag: null, 818 819 getPropTag: function(event) 820 { 821 var row = Dom.getAncestorByClass(event.target, "cssProp"); 822 var rule = Firebug.getRepObject(row); 823 var propName = Dom.getChildByClass(row, "cssPropName").textContent; 824 return { 825 a: rule, b: propName, 826 equals: function(other) 827 { 828 return (other && this.a === other.a && this.b === other.b); 829 } 830 }; 831 }, 832 833 clickedDisableButton: function(event) 834 { 835 // XXX hack 836 if (event.clientX > 20) 837 return false; 838 if (Css.hasClass(event.target, "textEditor inlineExpander")) 839 return false; 840 var row = Dom.getAncestorByClass(event.target, "cssProp"); 841 return (row && Css.hasClass(row, "editGroup")); 842 }, 843 844 onMouseDown: function(event) 845 { 846 this.clickedPropTag = null; 847 if (Events.isLeftClick(event) && this.clickedDisableButton(event)) 848 { 849 this.clickedPropTag = this.getPropTag(event); 850 851 // Don't select text when double-clicking the disable button. 852 Events.cancelEvent(event); 853 } 854 }, 855 856 onMouseUp: function(event) 857 { 858 if (Events.isLeftClick(event) && this.clickedDisableButton(event) && 859 this.getPropTag(event).equals(this.clickedPropTag)) 860 { 861 var row = Dom.getAncestorByClass(event.target, "cssProp"); 862 this.disablePropertyRow(row); 863 Events.cancelEvent(event); 864 } 865 this.clickedPropTag = null; 866 }, 867 868 onClick: function(event) 869 { 870 if (!Events.isLeftClick(event)) 871 return; 872 873 if (Events.isDoubleClick(event) && !this.clickedDisableButton(event)) 874 { 875 var row = Dom.getAncestorByClass(event.target, "cssRule"); 876 if (row && !Dom.getAncestorByClass(event.target, "cssPropName") 877 && !Dom.getAncestorByClass(event.target, "cssPropValue")) 878 { 879 this.insertPropertyRow(row); 880 Events.cancelEvent(event); 881 } 882 } 883 }, 884 885 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 886 // extends Panel 887 888 name: "stylesheet", 889 parentPanel: null, 890 searchable: true, 891 dependents: ["css", "stylesheet", "dom", "domSide", "layout"], 892 enableA11y: true, 893 deriveA11yFrom: "css", 894 order: 30, 895 896 initialize: function() 897 { 898 this.onMouseDown = Obj.bind(this.onMouseDown, this); 899 this.onMouseUp = Obj.bind(this.onMouseUp, this); 900 this.onClick = Obj.bind(this.onClick, this); 901 902 Firebug.Panel.initialize.apply(this, arguments); 903 }, 904 905 destroy: function(state) 906 { 907 state.scrollTop = this.panelNode.scrollTop ? this.panelNode.scrollTop : this.lastScrollTop; 908 909 Persist.persistObjects(this, state); 910 911 this.stopEditing(); 912 913 Firebug.Panel.destroy.apply(this, arguments); 914 }, 915 916 initializeNode: function(oldPanelNode) 917 { 918 Events.addEventListener(this.panelNode, "mousedown", this.onMouseDown, false); 919 Events.addEventListener(this.panelNode, "mouseup", this.onMouseUp, false); 920 Events.addEventListener(this.panelNode, "click", this.onClick, false); 921 922 Firebug.Panel.initializeNode.apply(this, arguments); 923 }, 924 925 destroyNode: function() 926 { 927 Events.removeEventListener(this.panelNode, "mousedown", this.onMouseDown, false); 928 Events.removeEventListener(this.panelNode, "mouseup", this.onMouseUp, false); 929 Events.removeEventListener(this.panelNode, "click", this.onClick, false); 930 931 Firebug.Panel.destroyNode.apply(this, arguments); 932 }, 933 934 show: function(state) 935 { 936 Firebug.Inspector.stopInspecting(true); 937 938 this.showToolbarButtons("fbCSSButtons", true); 939 940 CSSModule.updateEditButton(); 941 942 // wait for loadedContext to restore the panel 943 if (this.context.loaded && !this.location) 944 { 945 Persist.restoreObjects(this, state); 946 947 if (!this.location) 948 this.location = this.getDefaultLocation(); 949 950 if (state && state.scrollTop) 951 this.panelNode.scrollTop = state.scrollTop; 952 } 953 }, 954 955 hide: function() 956 { 957 this.lastScrollTop = this.panelNode.scrollTop; 958 }, 959 960 supportsObject: function(object, type) 961 { 962 if (object instanceof window.CSSStyleSheet) 963 { 964 return 1; 965 } 966 else if (object instanceof window.CSSRule || 967 (object instanceof window.CSSStyleDeclaration && object.parentRule) || 968 (object instanceof SourceLink.SourceLink && object.type == "css" && 969 Url.reCSS.test(object.href))) 970 { 971 return 2; 972 } 973 else 974 { 975 return 0; 976 } 977 }, 978 979 updateLocation: function(styleSheet) 980 { 981 if (FBTrace.DBG_CSS) 982 FBTrace.sysout("css.updateLocation; " + (styleSheet ? styleSheet.href : "no stylesheet")); 983 984 var rules = []; 985 if (styleSheet) 986 { 987 if (!Css.shouldIgnoreSheet(styleSheet)) 988 { 989 if (styleSheet.editStyleSheet) 990 styleSheet = styleSheet.editStyleSheet.sheet; 991 var rules = this.getStyleSheetRules(this.context, styleSheet); 992 } 993 } 994 995 if (rules.length) 996 { 997 this.template.tag.replace({rules: rules}, this.panelNode); 998 } 999 else 1000 { 1001 // If there are no rules on the page display a description that also 1002 // contains a link "create a rule". 1003 var warning = FirebugReps.Warning.tag.replace({object: ""}, this.panelNode); 1004 FirebugReps.Description.render(Locale.$STR("css.EmptyStyleSheet"), 1005 warning, Obj.bind(this.insertRule, this)); 1006 } 1007 1008 this.showToolbarButtons("fbCSSButtons", !Url.isSystemStyleSheet(this.location)); 1009 1010 Events.dispatch(this.fbListeners, "onCSSRulesAdded", [this, this.panelNode]); 1011 1012 // If the full editing mode (not the inline) is on while the location changes, 1013 // open the editor again for another file. 1014 if (this.editing && this.stylesheetEditor && this.stylesheetEditor.editing) 1015 { 1016 // Remove the editing flag to avoid recursion. The StylesheetEditor.endEditing 1017 // calls refresh and consequently updateLocation of the CSS panel. 1018 this.editing = null; 1019 1020 // Stop the current editing. 1021 this.stopEditing(); 1022 1023 // ... and open the editor again. 1024 this.toggleEditing(); 1025 } 1026 }, 1027 1028 updateSelection: function(object) 1029 { 1030 this.selection = null; 1031 1032 if (object instanceof window.CSSStyleDeclaration) 1033 { 1034 object = object.parentRule; 1035 } 1036 1037 if (object instanceof window.CSSRule) 1038 { 1039 this.navigate(object.parentStyleSheet); 1040 this.highlightRule(object); 1041 } 1042 else if (object instanceof window.CSSStyleSheet) 1043 { 1044 this.navigate(object); 1045 } 1046 else if (object instanceof SourceLink.SourceLink) 1047 { 1048 try 1049 { 1050 var sourceLink = object; 1051 1052 var sourceFile = Firebug.SourceFile.getSourceFileByHref(sourceLink.href, this.context); 1053 if (sourceFile) 1054 { 1055 Dom.clearNode(this.panelNode); // replace rendered stylesheets 1056 this.showSourceFile(sourceFile); 1057 1058 var lineNo = object.line; 1059 if (lineNo) 1060 this.scrollToLine(lineNo, this.jumpHighlightFactory(lineNo, this.context)); 1061 } 1062 else // XXXjjb we should not be taking this path 1063 { 1064 var stylesheet = Css.getStyleSheetByHref(sourceLink.href, this.context); 1065 if (stylesheet) 1066 { 1067 this.navigate(stylesheet); 1068 } 1069 else 1070 { 1071 if (FBTrace.DBG_CSS) 1072 FBTrace.sysout("css.updateSelection no sourceFile for " + 1073 sourceLink.href, sourceLink); 1074 } 1075 } 1076 } 1077 catch(exc) 1078 { 1079 if (FBTrace.DBG_CSS) 1080 FBTrace.sysout("css.upDateSelection FAILS "+exc, exc); 1081 } 1082 } 1083 }, 1084 1085 updateOption: function(name, value) 1086 { 1087 if (name == "expandShorthandProps" || name == "colorDisplay") 1088 this.refresh(); 1089 }, 1090 1091 getLocationList: function() 1092 { 1093 var styleSheets = Css.getAllStyleSheets(this.context); 1094 return styleSheets; 1095 }, 1096 1097 getOptionsMenuItems: function() 1098 { 1099 items = [ 1100 Menu.optionMenu("Expand_Shorthand_Properties", "expandShorthandProps", 1101 "css.option.tip.Expand_Shorthand_Properties") 1102 ]; 1103 1104 items = Arr.extendArray(items, CSSModule.getColorDisplayOptionMenuItems()); 1105 1106 items.push( 1107 "-", 1108 { 1109 label: "Refresh", 1110 tooltiptext: "panel.tip.Refresh", 1111 command: Obj.bind(this.refresh, this) 1112 } 1113 ); 1114 1115 return items; 1116 }, 1117 1118 getContextMenuItems: function(style, target) 1119 { 1120 var items = []; 1121 1122 if (target.nodeName == "TEXTAREA") 1123 { 1124 items = Firebug.BaseEditor.getContextMenuItems(); 1125 items.push( 1126 "-", 1127 { 1128 label: "Load_Original_Source", 1129 tooltiptext: "css.tip.Load_Original_Source", 1130 command: Obj.bindFixed(this.loadOriginalSource, this) 1131 } 1132 ); 1133 return items; 1134 } 1135 1136 if (Css.hasClass(target, "cssSelector")) 1137 { 1138 items.push( 1139 { 1140 label: "Copy_Rule_Declaration", 1141 tooltiptext: "css.tip.Copy_Rule_Declaration", 1142 id: "fbCopyRuleDeclaration", 1143 command: Obj.bindFixed(this.copyRuleDeclaration, this, target) 1144 }, 1145 { 1146 label: "Copy_Style_Declaration", 1147 tooltiptext: "css.tip.Copy_Style_Declaration", 1148 id: "fbCopyStyleDeclaration", 1149 command: Obj.bindFixed(this.copyStyleDeclaration, this, target) 1150 } 1151 ); 1152 } 1153 1154 var propValue = Dom.getAncestorByClass(target, "cssPropValue"); 1155 if (propValue) 1156 { 1157 if (this.infoTipType == "color") 1158 { 1159 items.push( 1160 { 1161 label: "CopyColor", 1162 tooltiptext: "css.tip.Copy_Color", 1163 command: Obj.bindFixed(System.copyToClipboard, System, this.infoTipObject) 1164 } 1165 ); 1166 } 1167 else if (this.infoTipType == "image") 1168 { 1169 items.push( 1170 { 1171 label: "CopyImageLocation", 1172 tooltiptext: "css.tip.Copy_Image_Location", 1173 command: Obj.bindFixed(System.copyToClipboard, System, this.infoTipObject) 1174 }, 1175 { 1176 label: "OpenImageInNewTab", 1177 tooltiptext: "css.tip.Open_Image_In_New_Tab", 1178 command: Obj.bindFixed(Win.openNewTab, Win, this.infoTipObject) 1179 } 1180 ); 1181 } 1182 } 1183 1184 if (!Url.isSystemStyleSheet(this.selection)) 1185 { 1186 items.push( 1187 "-", 1188 { 1189 label: "NewRule", 1190 tooltiptext: "css.tip.New_Rule", 1191 id: "fbNewCSSRule", 1192 command: Obj.bindFixed(this.insertRule, this, target) 1193 } 1194 ); 1195 } 1196 1197 if (Css.hasClass(target, "cssSelector")) 1198 { 1199 var selector = Str.cropString(target.textContent, 30); 1200 items.push( 1201 { 1202 label: Locale.$STRF("css.Delete_Rule", [selector]), 1203 tooltiptext: Locale.$STRF("css.tip.Delete_Rule", [selector]), 1204 nol10n: true, 1205 id: "fbDeleteRuleDeclaration", 1206 command: Obj.bindFixed(this.deleteRuleDeclaration, this, target) 1207 } 1208 ); 1209 } 1210 1211 var cssRule = Dom.getAncestorByClass(target, "cssRule"); 1212 if (cssRule) 1213 { 1214 if(Css.hasClass(cssRule, "cssEditableRule")) 1215 { 1216 items.push( 1217 "-", 1218 { 1219 label: "NewProp", 1220 tooltiptext: "css.tip.New_Prop", 1221 id: "fbNewCSSProp", 1222 command: Obj.bindFixed(this.insertPropertyRow, this, target) 1223 } 1224 ); 1225 1226 var propRow = Dom.getAncestorByClass(target, "cssProp"); 1227 if (propRow) 1228 { 1229 var propName = Dom.getChildByClass(propRow, "cssPropName").textContent; 1230 var isDisabled = Css.hasClass(propRow, "disabledStyle"); 1231 1232 items.push( 1233 { 1234 label: Locale.$STRF("EditProp", [propName]), 1235 tooltiptext: Locale.$STRF("css.tip.Edit_Prop", [propName]), 1236 nol10n: true, 1237 command: Obj.bindFixed(this.editPropertyRow, this, propRow) 1238 }, 1239 { 1240 label: Locale.$STRF("DeleteProp", [propName]), 1241 tooltiptext: Locale.$STRF("css.tip.Delete_Prop", [propName]), 1242 id: "fbDeleteCSSProp", 1243 nol10n: true, 1244 command: Obj.bindFixed(this.deletePropertyRow, this, propRow) 1245 }, 1246 { 1247 id: "fbDisableCSSProp", 1248 label: Locale.$STRF("DisableProp", [propName]), 1249 tooltiptext: Locale.$STRF("css.tip.Disable_Prop", [propName]), 1250 nol10n: true, 1251 type: "checkbox", 1252 checked: isDisabled, 1253 command: Obj.bindFixed(this.disablePropertyRow, this, propRow) 1254 } 1255 ); 1256 } 1257 } 1258 1259 if (Css.hasClass(cssRule, "importRule")) 1260 { 1261 items.push( 1262 { 1263 label: "css.menu.Edit_Media_Query", 1264 tooltiptext: "css.menu.tip.Edit_Media_Query", 1265 id: "fbEditMediaQuery", 1266 command: Obj.bindFixed(this.editMediaQuery, this, target) 1267 } 1268 ); 1269 } 1270 } 1271 1272 items.push( 1273 "-", 1274 { 1275 id: "fbRefresh", 1276 label: "Refresh", 1277 command: Obj.bind(this.refresh, this), 1278 tooltiptext: "panel.tip.Refresh" 1279 } 1280 ); 1281 1282 return items; 1283 }, 1284 1285 browseObject: function(object) 1286 { 1287 if (this.infoTipType == "image") 1288 { 1289 Win.openNewTab(this.infoTipObject); 1290 return true; 1291 } 1292 }, 1293 1294 showInfoTip: function(infoTip, target, x, y, rangeParent, rangeOffset) 1295 { 1296 var propValue = Dom.getAncestorByClass(target, "cssPropValue"); 1297 if (propValue) 1298 { 1299 var prop = Dom.getAncestorByClass(target, "cssProp"); 1300 var styleRule = Firebug.getRepObject(prop); 1301 var propNameNode = prop.getElementsByClassName("cssPropName").item(0); 1302 var propName = propNameNode.textContent.toLowerCase(); 1303 var priority = styleRule.style.getPropertyPriority(propName); 1304 var text = styleRule.style.getPropertyValue(propName) + 1305 (priority ? " !" + priority : ""); 1306 1307 if (text != "") 1308 { 1309 text = formatColor(text); 1310 } 1311 else 1312 { 1313 var disabledMap = this.getDisabledMap(this.context); 1314 var disabledProps = disabledMap.get(styleRule); 1315 if (disabledProps) 1316 { 1317 for (var i = 0, len = disabledProps.length; i < len; ++i) 1318 { 1319 if (disabledProps[i].name == propName) 1320 { 1321 priority = disabledProps[i].important; 1322 text = disabledProps[i].value + (priority ? " !" + priority : ""); 1323 break; 1324 } 1325 } 1326 } 1327 } 1328 var cssValue; 1329 1330 if (propName == "font" || propName == "font-family") 1331 { 1332 if (text.charAt(rangeOffset) == ",") 1333 return; 1334 1335 cssValue = CSSModule.parseCSSFontFamilyValue(text, rangeOffset, propName); 1336 } 1337 else 1338 { 1339 cssValue = CSSModule.parseCSSValue(text, rangeOffset); 1340 } 1341 1342 if (!cssValue) 1343 return false; 1344 1345 if (cssValue.value == this.infoTipValue) 1346 return true; 1347 1348 this.infoTipValue = cssValue.value; 1349 1350 switch (cssValue.type) 1351 { 1352 case "rgb": 1353 case "hsl": 1354 case "gradient": 1355 case "colorKeyword": 1356 this.infoTipType = "color"; 1357 this.infoTipObject = cssValue.value; 1358 return CSSInfoTip.populateColorInfoTip(infoTip, cssValue.value); 1359 1360 case "url": 1361 if (Css.isImageRule(Xml.getElementSimpleType(Firebug.getRepObject(target)), 1362 propNameNode.textContent)) 1363 { 1364 var prop = Dom.getAncestorByClass(target, "cssProp"); 1365 var rule = Firebug.getRepObject(prop); 1366 var baseURL = this.getStylesheetURL(rule, true); 1367 var relURL = CSSModule.parseURLValue(cssValue.value); 1368 var absURL = Url.isDataURL(relURL) ? relURL : Url.absoluteURL(relURL, baseURL); 1369 var repeat = CSSModule.parseRepeatValue(text); 1370 1371 this.infoTipType = "image"; 1372 this.infoTipObject = absURL; 1373 1374 return CSSInfoTip.populateImageInfoTip(infoTip, absURL, repeat); 1375 } 1376 break; 1377 1378 case "fontFamily": 1379 return CSSInfoTip.populateFontFamilyInfoTip(infoTip, cssValue.value); 1380 } 1381 1382 delete this.infoTipType; 1383 delete this.infoTipValue; 1384 delete this.infoTipObject; 1385 1386 return false; 1387 } 1388 }, 1389 1390 getEditor: function(target, value) 1391 { 1392 if (target == this.panelNode 1393 || Css.hasClass(target, "cssSelector") || Css.hasClass(target, "cssRule") 1394 || Css.hasClass(target, "cssSheet")) 1395 { 1396 if (!this.ruleEditor) 1397 this.ruleEditor = new CSSRuleEditor(this.document); 1398 1399 return this.ruleEditor; 1400 } 1401 else 1402 { 1403 if (!this.editor) 1404 this.editor = new CSSEditor(this.document); 1405 1406 return this.editor; 1407 } 1408 }, 1409 1410 getDefaultLocation: function() 1411 { 1412 // Note: We can't do makeDefaultStyleSheet here, because that could be 1413 // damaging for special pages (see e.g. issues 2440, 3688). 1414 try 1415 { 1416 var styleSheets = this.context.window.document.styleSheets; 1417 if (styleSheets.length) 1418 { 1419 var sheet = styleSheets[0]; 1420 return (Firebug.filterSystemURLs && 1421 Url.isSystemURL(Css.getURLForStyleSheet(sheet))) ? null : sheet; 1422 } 1423 } 1424 catch (exc) 1425 { 1426 if (FBTrace.DBG_LOCATIONS) 1427 FBTrace.sysout("css.getDefaultLocation FAILS "+exc, exc); 1428 } 1429 }, 1430 1431 getObjectLocation: function(styleSheet) 1432 { 1433 return Css.getURLForStyleSheet(styleSheet); 1434 }, 1435 1436 getObjectDescription: function(styleSheet) 1437 { 1438 var url = Css.getURLForStyleSheet(styleSheet); 1439 var instance = Css.getInstanceForStyleSheet(styleSheet); 1440 1441 var baseDescription = Url.splitURLBase(url); 1442 if (instance) { 1443 baseDescription.name = baseDescription.name + " #" + (instance + 1); 1444 } 1445 return baseDescription; 1446 }, 1447 1448 getSourceLink: function(target, rule) 1449 { 1450 var element = rule.parentStyleSheet.ownerNode; 1451 var href = rule.parentStyleSheet.href; // Null means inline 1452 1453 // http://code.google.com/p/fbug/issues/detail?id=452 1454 if (!href) 1455 href = element.ownerDocument.location.href; 1456 1457 var line = getRuleLine(rule); 1458 var instance = Css.getInstanceForStyleSheet(rule.parentStyleSheet); 1459 var sourceLink = new SourceLink.SourceLink(href, line, "css", rule, instance); 1460 1461 return sourceLink; 1462 }, 1463 1464 getTopmostRuleLine: function() 1465 { 1466 var panelNode = this.panelNode; 1467 for (var child = panelNode.firstChild; child; child = child.nextSibling) 1468 { 1469 if (child.offsetTop+child.offsetHeight > panelNode.scrollTop) 1470 { 1471 var rule = child.repObject; 1472 if (rule) 1473 { 1474 return { 1475 line: getRuleLine(rule), 1476 offset: panelNode.scrollTop-child.offsetTop 1477 }; 1478 } 1479 } 1480 } 1481 return 0; 1482 }, 1483 1484 getCurrentLineNumber: function() 1485 { 1486 var ruleLine = this.getTopMostRuleLine(); 1487 if (ruleLine) 1488 return ruleLine.line; 1489 }, 1490 1491 search: function(text, reverse) 1492 { 1493 var curDoc = this.searchCurrentDoc(!Firebug.searchGlobal, text, reverse); 1494 if (!curDoc && Firebug.searchGlobal) 1495 { 1496 return this.searchOtherDocs(text, reverse) || 1497 this.searchCurrentDoc(true, text, reverse); 1498 } 1499 return curDoc; 1500 }, 1501 1502 searchOtherDocs: function(text, reverse) 1503 { 1504 var scanRE = Firebug.Search.getTestingRegex(text); 1505 function scanDoc(styleSheet) { 1506 // we don't care about reverse here as we are just looking for existence, 1507 // if we do have a result we will handle the reverse logic on display 1508 for (var i = 0; i < styleSheet.cssRules.length; i++) 1509 { 1510 if (scanRE.test(styleSheet.cssRules[i].cssText)) 1511 { 1512 return true; 1513 } 1514 } 1515 } 1516 1517 if (this.navigateToNextDocument(scanDoc, reverse)) 1518 { 1519 // firefox findService can't find nodes immediatly after insertion 1520 setTimeout(Obj.bind(this.searchCurrentDoc, this), 0, true, text, reverse); 1521 return "wraparound"; 1522 } 1523 }, 1524 1525 searchCurrentDoc: function(wrapSearch, text, reverse) 1526 { 1527 var row, sel; 1528 1529 if (!text) 1530 { 1531 delete this.currentSearch; 1532 this.highlightNode(null); 1533 this.document.defaultView.getSelection().removeAllRanges(); 1534 return false; 1535 } 1536 1537 if (this.currentSearch && text == this.currentSearch.text) 1538 { 1539 row = this.currentSearch.findNext(wrapSearch, false, reverse, 1540 Firebug.Search.isCaseSensitive(text)); 1541 } 1542 else 1543 { 1544 if (this.editing) 1545 { 1546 this.currentSearch = new Search.TextSearch(this.stylesheetEditor.box); 1547 row = this.currentSearch.find(text, reverse, Firebug.Search.isCaseSensitive(text)); 1548 1549 if (row) 1550 { 1551 sel = this.document.defaultView.getSelection(); 1552 sel.removeAllRanges(); 1553 sel.addRange(this.currentSearch.range); 1554 1555 scrollSelectionIntoView(this); 1556 this.highlightNode(row); 1557 1558 return true; 1559 } 1560 else 1561 { 1562 return false; 1563 } 1564 } 1565 else 1566 { 1567 function findRow(node) { 1568 return node.nodeType == Node.ELEMENT_NODE ? node : node.parentNode; 1569 } 1570 1571 this.currentSearch = new Search.TextSearch(this.panelNode, findRow); 1572 row = this.currentSearch.find(text, reverse, Firebug.Search.isCaseSensitive(text)); 1573 } 1574 } 1575 1576 if (row) 1577 { 1578 sel = this.document.defaultView.getSelection(); 1579 sel.removeAllRanges(); 1580 sel.addRange(this.currentSearch.range); 1581 1582 // Should be replaced by scrollToLine() of sourceBox, 1583 // though first jumpHighlightFactory() has to be adjusted to 1584 // remove the current highlighting when called again 1585 Dom.scrollIntoCenterView(row, this.panelNode); 1586 this.highlightNode(row.parentNode); 1587 1588 Events.dispatch(this.fbListeners, "onCSSSearchMatchFound", [this, text, row]); 1589 return this.currentSearch.wrapped ? "wraparound" : true; 1590 } 1591 else 1592 { 1593 this.document.defaultView.getSelection().removeAllRanges(); 1594 Events.dispatch(this.fbListeners, "onCSSSearchMatchFound", [this, text, null]); 1595 return false; 1596 } 1597 }, 1598 1599 getSearchOptionsMenuItems: function() 1600 { 1601 return [ 1602 Firebug.Search.searchOptionMenu("search.Case_Sensitive", "searchCaseSensitive", 1603 "search.tip.Case_Sensitive"), 1604 Firebug.Search.searchOptionMenu("search.Multiple_Files", "searchGlobal", 1605 "search.tip.Multiple_Files"), 1606 Firebug.Search.searchOptionMenu("search.Use_Regular_Expression", 1607 "searchUseRegularExpression", "search.tip.Use_Regular_Expression") 1608 ]; 1609 }, 1610 1611 getStyleDeclaration: function(cssSelector) 1612 { 1613 var cssRule = Dom.getAncestorByClass(cssSelector, "cssRule"); 1614 var propRows = cssRule.getElementsByClassName("cssProp"); 1615 1616 var lines = []; 1617 for (var i = 0; i < propRows.length; ++i) 1618 { 1619 var row = propRows[i]; 1620 if (row.classList.contains("disabledStyle")) 1621 continue; 1622 1623 var name = Dom.getChildByClass(row, "cssPropName").textContent; 1624 var value = Dom.getChildByClass(row, "cssPropValue").textContent; 1625 lines.push(name + ": " + value + ";"); 1626 } 1627 1628 return lines; 1629 }, 1630 1631 copyRuleDeclaration: function(cssSelector) 1632 { 1633 var props = this.getStyleDeclaration(cssSelector); 1634 System.copyToClipboard(cssSelector.textContent + " {" + Str.lineBreak() + " " + 1635 props.join(Str.lineBreak() + " ") + Str.lineBreak() + "}"); 1636 }, 1637 1638 deleteRuleDeclaration: function(cssSelector) 1639 { 1640 var searchRule = Firebug.getRepObject(cssSelector) || 1641 Firebug.getRepObject(cssSelector.nextSibling); 1642 var styleSheet = searchRule.parentRule || searchRule.parentStyleSheet; 1643 var ruleIndex = 0; 1644 var cssRules = styleSheet.cssRules; 1645 while (ruleIndex < cssRules.length && searchRule != cssRules[ruleIndex]) 1646 ruleIndex++; 1647 1648 if (FBTrace.DBG_CSS) 1649 { 1650 FBTrace.sysout("css.deleteRuleDeclaration; selector: "+ 1651 Str.cropString(cssSelector.textContent, 100), 1652 {styleSheet: styleSheet, ruleIndex: ruleIndex}); 1653 } 1654 1655 CSSModule.deleteRule(styleSheet, ruleIndex); 1656 1657 var rule = Dom.getAncestorByClass(cssSelector, "cssRule"); 1658 if (rule) 1659 rule.parentNode.removeChild(rule); 1660 }, 1661 1662 copyStyleDeclaration: function(cssSelector) 1663 { 1664 var props = this.getStyleDeclaration(cssSelector); 1665 System.copyToClipboard(props.join(Str.lineBreak())); 1666 } 1667 }); 1668 1669 // ********************************************************************************************* // 1670 // CSSEditor 1671 1672 function CSSEditor(doc) 1673 { 1674 this.initializeInline(doc); 1675 } 1676 1677 CSSEditor.prototype = domplate(Firebug.InlineEditor.prototype, 1678 { 1679 insertNewRow: function(target, insertWhere) 1680 { 1681 var rule = Firebug.getRepObject(target); 1682 if (!rule) 1683 { 1684 if (FBTrace.DBG_CSS) 1685 FBTrace.sysout("CSSEditor.insertNewRow; ERROR There is no CSS rule", target); 1686 return; 1687 } 1688 1689 var emptyProp = {name: "", value: "", important: ""}; 1690 1691 if (insertWhere == "before") 1692 return CSSPropTag.tag.insertBefore({prop: emptyProp, rule: rule}, target); 1693 else 1694 return CSSPropTag.tag.insertAfter({prop: emptyProp, rule: rule}, target); 1695 }, 1696 1697 saveEdit: function(target, value, previousValue) 1698 { 1699 if (FBTrace.DBG_CSS) 1700 FBTrace.sysout("CSSEditor.saveEdit", arguments); 1701 1702 var cssRule = Dom.getAncestorByClass(target, "cssRule"); 1703 var rule = Firebug.getRepObject(cssRule); 1704 1705 if (rule instanceof window.CSSStyleRule || rule instanceof window.Element) 1706 { 1707 var prop = Dom.getAncestorByClass(target, "cssProp"); 1708 1709 if (prop) 1710 { 1711 var propName = Dom.getChildByClass(prop, "cssPropName").textContent; 1712 // If the property was previously disabled, remove it from the "disabled" 1713 // map. (We will then proceed to enable the property.) 1714 if (prop && prop.classList.contains("disabledStyle")) 1715 { 1716 prop.classList.remove("disabledStyle"); 1717 1718 this.panel.removeDisabledProperty(rule, propName); 1719 } 1720 1721 if (Css.hasClass(target, "cssPropName")) 1722 { 1723 // Actual saving is done in endEditing, see the comment there. 1724 target.textContent = value; 1725 } 1726 else if (Dom.getAncestorByClass(target, "cssPropValue")) 1727 { 1728 target.textContent = CSSDomplateBase.getPropertyValue({value: value}); 1729 1730 propName = Dom.getChildByClass(prop, "cssPropName").textContent; 1731 1732 if (FBTrace.DBG_CSS) 1733 { 1734 FBTrace.sysout("CSSEditor.saveEdit \"" + propName + "\" = \"" + value + "\""); 1735 // FBTrace.sysout("CSSEditor.saveEdit BEFORE style:",style); 1736 } 1737 1738 if (value && value != "null") 1739 { 1740 var parsedValue = parsePriority(value); 1741 CSSModule.setProperty(rule, propName, parsedValue.value, 1742 parsedValue.priority); 1743 } 1744 else if (previousValue && previousValue != "null") 1745 { 1746 CSSModule.removeProperty(rule, propName); 1747 } 1748 } 1749 1750 if (value) 1751 { 1752 var saveSuccess = false; 1753 if (Css.hasClass(target, "cssPropName")) 1754 { 1755 var propName = value.replace(/-./g, function(match) 1756 { 1757 return match[1].toUpperCase(); 1758 }); 1759 1760 if (propName in rule.style || propName == "float") 1761 saveSuccess = "almost"; 1762 } 1763 else 1764 { 1765 saveSuccess = !!rule.style.getPropertyValue(propName); 1766 } 1767 1768 this.box.setAttribute("saveSuccess", saveSuccess); 1769 } 1770 else 1771 { 1772 this.box.removeAttribute("saveSuccess"); 1773 } 1774 } 1775 } 1776 else if (rule instanceof window.CSSImportRule && Css.hasClass(target, "cssMediaQuery")) 1777 { 1778 target.textContent = value; 1779 1780 if (FBTrace.DBG_CSS) 1781 { 1782 FBTrace.sysout("CSSEditor.saveEdit: @import media query: " + 1783 previousValue + "->" + value); 1784 } 1785 1786 rule.media.mediaText = value; 1787 1788 // Workaround to apply the media query changes 1789 rule.parentStyleSheet.disabled = true; 1790 rule.parentStyleSheet.disabled = false; 1791 1792 var row = Dom.getAncestorByClass(target, "importRule"); 1793 row.getElementsByClassName("separator").item(0).textContent = 1794 value == "" ? "" : String.fromCharCode(160); 1795 1796 var saveSuccess = rule.media.mediaText != "not all" || value == "not all"; 1797 this.box.setAttribute("saveSuccess", saveSuccess); 1798 } 1799 else if (rule instanceof window.CSSCharsetRule) 1800 { 1801 target.textContent = value; 1802 1803 if (FBTrace.DBG_CSS) 1804 FBTrace.sysout("CSSEditor.saveEdit: @charset: " + previousValue + "->" + value); 1805 1806 rule.encoding = value; 1807 } 1808 1809 Firebug.Inspector.repaint(); 1810 1811 this.panel.markChange(this.panel.name == "stylesheet"); 1812 1813 if (FBTrace.DBG_CSS) 1814 FBTrace.sysout("CSSEditor.saveEdit (ending) " + this.panel.name, value); 1815 }, 1816 1817 beginEditing: function(target, value) 1818 { 1819 var row = Dom.getAncestorByClass(target, "cssProp"); 1820 this.initialValue = value; 1821 this.initiallyDisabled = (row && row.classList.contains("disabledStyle")); 1822 }, 1823 1824 endEditing: function(target, value, cancel) 1825 { 1826 if (!cancel && target.classList.contains("cssPropName")) 1827 { 1828 // Save changed property names here instead of in saveEdit, because otherwise 1829 // unrelated properties might get discarded (see issue 5204). 1830 var previous = this.initialValue; 1831 if (FBTrace.DBG_CSS) 1832 { 1833 FBTrace.sysout("CSSEditor.endEditing: renaming property " + previous + " -> " + value); 1834 } 1835 1836 var cssRule = Dom.getAncestorByClass(target, "cssRule"); 1837 var rule = Firebug.getRepObject(cssRule); 1838 var baseText = rule.style ? rule.style.cssText : rule.cssText; 1839 var prop = Dom.getAncestorByClass(target, "cssProp"); 1840 var propValue = Dom.getChildByClass(prop, "cssPropValue").textContent; 1841 var parsedValue = parsePriority(propValue); 1842 1843 if (previous) 1844 CSSModule.removeProperty(rule, previous); 1845 if (propValue) 1846 CSSModule.setProperty(rule, value, parsedValue.value, parsedValue.priority); 1847 1848 Events.dispatch(CSSModule.fbListeners, "onCSSPropertyNameChanged", [rule, value, 1849 previous, baseText]); 1850 1851 Firebug.Inspector.repaint(); 1852 this.panel.markChange(this.panel.name == "stylesheet"); 1853 } 1854 return true; 1855 }, 1856 1857 cancelEditing: function(target, value) 1858 { 1859 if (this.initiallyDisabled) 1860 { 1861 // Disable the property again. 1862 var row = Dom.getAncestorByClass(target, "cssProp"); 1863 if (row && !row.classList.contains("disabledStyle")) 1864 this.panel.disablePropertyRow(row); 1865 } 1866 }, 1867 1868 advanceToNext: function(target, charCode) 1869 { 1870 if (charCode == 58 /*":"*/ && Css.hasClass(target, "cssPropName")) 1871 { 1872 return true; 1873 } 1874 else if (charCode == 59 /*";"*/ && Css.hasClass(target, "cssPropValue")) 1875 { 1876 var cssValue = CSSModule.parseCSSValue(this.input.value, this.input.selectionStart); 1877 // Simple test, if we are inside a string (see issue 4543) 1878 var isValueInString = (cssValue.value.indexOf("\"") != -1); 1879 1880 return !isValueInString; 1881 } 1882 }, 1883 1884 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1885 1886 getAutoCompleteRange: function(value, offset) 1887 { 1888 if (!Css.hasClass(this.target, "cssPropValue")) 1889 return {start: 0, end: value.length}; 1890 1891 var propRow = Dom.getAncestorByClass(this.target, "cssProp"); 1892 var propName = Dom.getChildByClass(propRow, "cssPropName").textContent.toLowerCase(); 1893 1894 if (propName == "font" || propName == "font-family") 1895 return CSSModule.parseCSSFontFamilyValue(value, offset, propName); 1896 else 1897 return CSSModule.parseCSSValue(value, offset); 1898 }, 1899 1900 getAutoCompleteList: function(preExpr, expr, postExpr, range, cycle, context, out) 1901 { 1902 if (Dom.getAncestorByClass(this.target, "importRule")) 1903 { 1904 return []; 1905 } 1906 else if (Dom.getAncestorByClass(this.target, "cssCharsetRule")) 1907 { 1908 return Css.charsets; 1909 } 1910 else if (Css.hasClass(this.target, "cssPropName")) 1911 { 1912 var nodeType = Xml.getElementSimpleType(Firebug.getRepObject(this.target)); 1913 return Css.getCSSPropertyNames(nodeType); 1914 } 1915 else 1916 { 1917 if (expr.charAt(0) === "!") 1918 return ["!important"]; 1919 1920 var row = Dom.getAncestorByClass(this.target, "cssProp"); 1921 var propName = Dom.getChildByClass(row, "cssPropName").textContent; 1922 var nodeType = Xml.getElementSimpleType(Firebug.getRepObject(this.target)); 1923 1924 var keywords; 1925 if (range.type === "url") 1926 { 1927 // We can't complete urls yet. 1928 return []; 1929 } 1930 else if (range.type === "fontFamily") 1931 { 1932 keywords = Css.cssKeywords["fontFamily"].slice(); 1933 if (this.panel && this.panel.context) 1934 { 1935 // Add the fonts used in this context (they might be inaccessible 1936 // for this element, but probably aren't). 1937 var fonts = Fonts.getFontsUsedInContext(this.panel.context), ar = []; 1938 for (var i = 0; i < fonts.length; i++) 1939 ar.push(fonts[i].CSSFamilyName); 1940 keywords = Arr.sortUnique(keywords.concat(ar)); 1941 } 1942 1943 var q = expr.charAt(0), isQuoted = (q === '"' || q === "'"); 1944 if (!isQuoted) 1945 { 1946 // Default to ' quotes, unless " occurs somewhere. 1947 q = (/"/.test(preExpr + postExpr) ? '"' : "'"); 1948 } 1949 1950 // Don't complete '. 1951 if (expr.length <= 1 && isQuoted) 1952 return []; 1953 1954 // When completing, quote fonts if the input is quoted; when 1955 // cycling, quote them instead in the way the user seems to 1956 // expect to have them quoted. 1957 var reSimple = /^[a-z][a-z0-9-]*$/i; 1958 var isComplex = !reSimple.test(expr.replace(/^['"]?|['"]?$/g, "")); 1959 var quote = function(str) 1960 { 1961 if (!cycle || isComplex !== isQuoted) 1962 return (isQuoted ? q + str + q : str); 1963 else 1964 return (reSimple.test(str) ? str : q + str + q); 1965 }; 1966 1967 keywords = keywords.slice(); 1968 for (var i = 0; i < keywords.length; ++i) 1969 { 1970 // Treat values starting with capital letters as font names 1971 // that can be quoted. 1972 var k = keywords[i]; 1973 if (k.charAt(0).toLowerCase() !== k.charAt(0)) 1974 keywords[i] = quote(k); 1975 } 1976 } 1977 else 1978 { 1979 var lowerProp = propName.toLowerCase(), avoid; 1980 if (["background", "border", "font"].indexOf(lowerProp) !== -1) 1981 { 1982 if (cycle) 1983 { 1984 // Cycle only within the same category, if possible. 1985 var cat = Css.getCSSShorthandCategory(nodeType, lowerProp, expr); 1986 if (cat) 1987 return (cat in Css.cssKeywords ? Css.cssKeywords[cat] : [cat]); 1988 } 1989 else 1990 { 1991 // Avoid repeated properties. We assume the values to be solely 1992 // space-separated tokens, within a comma-separated part (like 1993 // for CSS3 multiple backgrounds). This is absolutely wrong, but 1994 // good enough in practice because non-tokens for which it fails 1995 // likely aren't in any category. 1996 // "background-position" and "background-repeat" values can occur 1997 // twice, so they are special-cased. 1998 avoid = []; 1999 var preTokens = preExpr.split(",").reverse()[0].split(" "); 2000 var postTokens = postExpr.split(",")[0].split(" "); 2001 var tokens = preTokens.concat(postTokens); 2002 for (var i = 0; i < tokens.length; ++i) 2003 { 2004 var cat = Css.getCSSShorthandCategory(nodeType, lowerProp, tokens[i]); 2005 if (cat && cat !== "position" && cat !== "bgRepeat") 2006 avoid.push(cat); 2007 } 2008 } 2009 } 2010 keywords = Css.getCSSKeywordsByProperty(nodeType, propName, avoid); 2011 } 2012 2013 // Add the magic inherit property, if it's sufficiently alone. 2014 // XXX Firefox 19 also has "initial" 2015 if (!preExpr) 2016 keywords = keywords.concat(["inherit"]); 2017 2018 if (!cycle) 2019 { 2020 // Make some good default suggestions. 2021 var list = ["white", "black", "solid", "outset", "repeat"]; 2022 for (var i = 0; i < list.length; ++i) 2023 { 2024 if (Str.hasPrefix(list[i], expr) && keywords.indexOf(list[i]) !== -1) 2025 { 2026 out.suggestion = list[i]; 2027 break; 2028 } 2029 } 2030 } 2031 2032 return SelectorEditor.stripCompletedParens(keywords, postExpr); 2033 } 2034 }, 2035 2036 getAutoCompletePropSeparator: function(range, expr, prefixOf) 2037 { 2038 if (!Css.hasClass(this.target, "cssPropValue")) 2039 return null; 2040 2041 // For non-multi-valued properties, fail (pre-completions don't make sense, 2042 // and it's less risky). 2043 var row = Dom.getAncestorByClass(this.target, "cssProp"); 2044 var propName = Dom.getChildByClass(row, "cssPropName").textContent; 2045 if (!Css.multiValuedProperties.hasOwnProperty(propName)) 2046 return null; 2047 2048 if (range.type === "fontFamily") 2049 return ","; 2050 return " "; 2051 }, 2052 2053 autoCompleteAdjustSelection: function(value, offset) 2054 { 2055 if (offset >= 2 && value.substr(offset-2, 2) === "()") 2056 return offset-1; 2057 return offset; 2058 }, 2059 2060 doIncrementValue: function(value, amt, offset, offsetEnd) 2061 { 2062 var propName = null; 2063 if (Css.hasClass(this.target, "cssPropValue")) 2064 { 2065 var propRow = Dom.getAncestorByClass(this.target, "cssProp"); 2066 propName = Dom.getChildByClass(propRow, "cssPropName").textContent; 2067 } 2068 2069 var range = CSSModule.parseCSSValue(value, offset); 2070 var type = (range && range.type) || ""; 2071 var expr = (range ? value.substring(range.start, range.end) : ""); 2072 2073 var completion = null, selection, info; 2074 if (type === "int") 2075 { 2076 if (propName === "opacity") 2077 { 2078 info = {minValue: 0, maxValue: 1}; 2079 amt /= 100; 2080 } 2081 2082 if (expr === "0" && value.lastIndexOf("(", offset) === -1 && 2083 !Css.unitlessProperties.hasOwnProperty(propName)) 2084 { 2085 // 0 is a length, and incrementing it normally will result in an 2086 // invalid value 1 or -1. Thus, guess at a unit to add. 2087 var unitM = /\d([a-z]{1,4})/.exec(value); 2088 expr += (unitM ? unitM[1] : "px"); 2089 } 2090 2091 var newValue = this.incrementExpr(expr, amt, info); 2092 if (newValue !== null) 2093 { 2094 completion = newValue; 2095 selection = [0, completion.length]; 2096 } 2097 } 2098 else if (type === "rgb" && expr.charAt(0) === "#") 2099 { 2100 var offsetIntoExpr = offset - range.start; 2101 var offsetEndIntoExpr = offsetEnd - range.start; 2102 2103 // Increment a hex color. 2104 var res = this.incrementHexColor(expr, amt, offsetIntoExpr, offsetEndIntoExpr); 2105 if (res) 2106 { 2107 completion = res.value; 2108 selection = res.selection; 2109 } 2110 } 2111 else 2112 { 2113 if (type === "rgb" || type === "hsl") 2114 { 2115 info = {}; 2116 var part = value.substring(range.start, offset).split(",").length - 1; 2117 if (part === 3) // alpha 2118 { 2119 info.minValue = 0; 2120 info.maxValue = 1; 2121 amt /= 100; 2122 } 2123 else if (type === "rgb") // rgb color 2124 { 2125 info.minValue = 0; 2126 info.maxValue = 255; 2127 if (Math.abs(amt) < 1) 2128 amt = (amt < 0 ? -1 : 1); 2129 } 2130 else if (part !== 0) // hsl percentage 2131 { 2132 info.minValue = 0; 2133 info.maxValue = 100; 2134 2135 // If the selection is at the end of a percentage sign, select 2136 // the previous number. This would have been less hacky if 2137 // parseCSSValue parsed functions recursively. 2138 if (value.charAt(offset-1) === "%") 2139 --offset; 2140 } 2141 } 2142 2143 return Firebug.InlineEditor.prototype.doIncrementValue 2144 .call(this, value, amt, offset, offsetEnd, info); 2145 } 2146 2147 if (completion === null) 2148 return; 2149 2150 var preExpr = value.substr(0, range.start); 2151 var postExpr = value.substr(range.end); 2152 2153 return { 2154 value: preExpr + completion + postExpr, 2155 start: range.start + selection[0], 2156 end: range.start + selection[1] 2157 }; 2158 }, 2159 2160 incrementHexColor: function(expr, amt, offset, offsetEnd) 2161 { 2162 // Return early if no part of the expression is selected. 2163 if (offsetEnd > expr.length && offset >= expr.length) 2164 return; 2165 if (offset < 1 && offsetEnd <= 1) 2166 return; 2167 2168 // Ignore the leading #. 2169 expr = expr.substr(1); 2170 --offset; 2171 --offsetEnd; 2172 2173 // Clamp the selection to within the actual value. 2174 offset = Math.max(offset, 0); 2175 offsetEnd = Math.min(offsetEnd, expr.length); 2176 offsetEnd = Math.max(offsetEnd, offset); 2177 2178 // Normalize #ABC -> #AABBCC. 2179 if (expr.length === 3) 2180 { 2181 expr = expr.charAt(0) + expr.charAt(0) + 2182 expr.charAt(1) + expr.charAt(1) + 2183 expr.charAt(2) + expr.charAt(2); 2184 offset *= 2; 2185 offsetEnd *= 2; 2186 } 2187 if (expr.length !== 6) 2188 return; 2189 2190 if (offset === offsetEnd) 2191 { 2192 // There is only a single cursor position. Increment an adjacent 2193 // color, preferably one to the left. 2194 if (offset === 0) 2195 offsetEnd = 1; 2196 else 2197 offset = offsetEnd - 1; 2198 } 2199 2200 // Make the selection cover entire parts. 2201 offset -= offset%2; 2202 offsetEnd += offsetEnd%2; 2203 2204 // Remap the increments from [0.1, 1, 10] to [1, 1, 16]. 2205 if (-1 < amt && amt < 1) 2206 amt = (amt < 0 ? -1 : 1); 2207 if (Math.abs(amt) === 10) 2208 amt = (amt < 0 ? -16 : 16); 2209 2210 var isUpper = (expr.toUpperCase() === expr); 2211 2212 for (var pos = offset; pos < offsetEnd; pos += 2) 2213 { 2214 // Increment the part in [pos, pos+2). 2215 var mid = expr.substr(pos, 2); 2216 var value = parseInt(mid, 16); 2217 if (isNaN(value)) 2218 return; 2219 2220 mid = Math.min(Math.max(value - amt, 0), 255).toString(16); 2221 while (mid.length < 2) 2222 mid = "0" + mid; 2223 2224 // Make the incremented part upper-case if the original value can be 2225 // seen as such (this should happen even for, say, #444444, because 2226 // upper-case hex-colors are the default). Otherwise, the lower-case 2227 // result from .toString is used. 2228 if (isUpper) 2229 mid = mid.toUpperCase(); 2230 2231 expr = expr.substr(0, pos) + mid + expr.substr(pos+2); 2232 } 2233 2234 return {value: "#" + expr, selection: [offset+1, offsetEnd+1]}; 2235 } 2236 }); 2237 2238 // ********************************************************************************************* // 2239 // CSSRuleEditor 2240 2241 function CSSRuleEditor(doc) 2242 { 2243 this.initializeInline(doc); 2244 } 2245 2246 CSSRuleEditor.prototype = domplate(SelectorEditor.prototype, 2247 { 2248 insertNewRow: function(target, insertWhere) 2249 { 2250 var emptyRule = { 2251 selector: "", 2252 id: "", 2253 props: [], 2254 isSelectorEditable: true 2255 }; 2256 2257 if (insertWhere == "before") 2258 return CSSStyleRuleTag.tag.insertBefore({rule: emptyRule}, target); 2259 else 2260 return CSSStyleRuleTag.tag.insertAfter({rule: emptyRule}, target); 2261 }, 2262 2263 saveEdit: function(target, value, previousValue) 2264 { 2265 var context = this.panel.context; 2266 2267 if (FBTrace.DBG_CSS) 2268 FBTrace.sysout("CSSRuleEditor.saveEdit: '" + value + "' '" + previousValue + 2269 "'", target); 2270 2271 target.innerHTML = Str.escapeForCss(value); 2272 if (value === previousValue) 2273 return; 2274 2275 var row = Dom.getAncestorByClass(target, "cssRule"); 2276 var rule = Firebug.getRepObject(target); 2277 2278 var searchRule = rule || Firebug.getRepObject(row.nextSibling); 2279 var oldRule, ruleIndex; 2280 2281 if (searchRule) 2282 { 2283 // take care of media rules 2284 var styleSheet = searchRule.parentRule || searchRule.parentStyleSheet; 2285 if (!styleSheet) 2286 return; 2287 2288 var cssRules = styleSheet.cssRules; 2289 for (ruleIndex=0; ruleIndex<cssRules.length && searchRule!=cssRules[ruleIndex]; 2290 ruleIndex++) 2291 { 2292 } 2293 2294 if (rule) 2295 oldRule = searchRule; 2296 else 2297 ruleIndex++; 2298 } 2299 else 2300 { 2301 var styleSheet; 2302 if (this.panel.name === "stylesheet") 2303 { 2304 styleSheet = this.panel.location; 2305 if (!styleSheet) 2306 { 2307 var doc = context.window.document; 2308 this.panel.location = styleSheet = 2309 CSSModule.getDefaultStyleSheet(doc); 2310 } 2311 } 2312 else 2313 { 2314 if (this.panel.name !== "css") 2315 return; 2316 2317 var doc = this.panel.selection.ownerDocument.defaultView.document; 2318 styleSheet = CSSModule.getDefaultStyleSheet(doc); 2319 } 2320 2321 styleSheet = styleSheet.editStyleSheet ? styleSheet.editStyleSheet.sheet : styleSheet; 2322 cssRules = styleSheet.cssRules; 2323 ruleIndex = cssRules.length; 2324 } 2325 2326 // Delete in all cases except for new add 2327 // We want to do this before the insert to ease change tracking 2328 if (oldRule) 2329 { 2330 CSSModule.deleteRule(styleSheet, ruleIndex); 2331 } 2332 2333 var doMarkChange = true; 2334 2335 // Firefox does not follow the spec for the update selector text case. 2336 // When attempting to update the value, firefox will silently fail. 2337 // See https://bugzilla.mozilla.org/show_bug.cgi?id=37468 for the quite 2338 // old discussion of this bug. 2339 // As a result we need to recreate the style every time the selector 2340 // changes. 2341 if (value) 2342 { 2343 var cssText = [ value, "{" ]; 2344 var props = row.getElementsByClassName("cssProp"); 2345 for (var i = 0; i < props.length; i++) 2346 { 2347 2348 var propEl = props[i]; 2349 if (!Css.hasClass(propEl, "disabledStyle")) 2350 { 2351 var propName = Dom.getChildByClass(propEl, "cssPropName").textContent; 2352 var propValue = Dom.getChildByClass(propEl, "cssPropValue").textContent; 2353 cssText.push(propName + ":" + propValue + ";"); 2354 } 2355 } 2356 2357 cssText.push("}"); 2358 cssText = cssText.join(""); 2359 2360 try 2361 { 2362 var insertLoc = CSSModule.insertRule(styleSheet, cssText, ruleIndex); 2363 2364 rule = cssRules[insertLoc]; 2365 2366 ruleIndex++; 2367 2368 var saveSuccess = (this.panel.name != "css"); 2369 if (!saveSuccess) 2370 { 2371 saveSuccess = (this.panel.selection && 2372 this.panel.selection.mozMatchesSelector(value)) ? true : 'almost'; 2373 } 2374 2375 this.box.setAttribute('saveSuccess', saveSuccess); 2376 } 2377 catch (err) 2378 { 2379 if (FBTrace.DBG_CSS || FBTrace.DBG_ERRORS) 2380 FBTrace.sysout("CSS Insert Error: "+err, err); 2381 2382 target.innerHTML = Str.escapeForCss(previousValue); 2383 // create dummy rule to be able to recover from error 2384 var insertLoc = CSSModule.insertRule(styleSheet, 2385 'selectorSavingError{}', ruleIndex); 2386 rule = cssRules[insertLoc]; 2387 2388 this.box.setAttribute('saveSuccess', false); 2389 2390 doMarkChange = false; 2391 } 2392 } 2393 else 2394 { 2395 // XXX There is currently no way to re-add the rule after this happens. 2396 rule = undefined; 2397 } 2398 2399 // Update the rep object 2400 row.repObject = rule; 2401 if (oldRule && rule) 2402 this.panel.remapRule(context, oldRule, rule); 2403 2404 if (doMarkChange) 2405 this.panel.markChange(this.panel.name == "stylesheet"); 2406 }, 2407 2408 getAutoCompleteRange: function(value, offset) 2409 { 2410 if (!Css.hasClass(this.target, "cssSelector")) 2411 return; 2412 return SelectorEditor.prototype.getAutoCompleteRange.apply(this, arguments); 2413 }, 2414 2415 getAutoCompleteList: function(preExpr, expr, postExpr, range, cycle, context, out) 2416 { 2417 if (!Css.hasClass(this.target, "cssSelector")) 2418 return []; 2419 return SelectorEditor.prototype.getAutoCompleteList.apply(this, arguments); 2420 }, 2421 2422 getAutoCompletePropSeparator: function(range, expr, prefixOf) 2423 { 2424 if (!Css.hasClass(this.target, "cssSelector")) 2425 return null; 2426 return SelectorEditor.prototype.getAutoCompletePropSeparator.apply(this, arguments); 2427 }, 2428 2429 advanceToNext: function(target, charCode) 2430 { 2431 if (charCode == 123 /* "{" */) 2432 { 2433 return true; 2434 } 2435 } 2436 }); 2437 2438 // ********************************************************************************************* // 2439 // StyleSheetEditor 2440 2441 /** 2442 * StyleSheetEditor represents the full-sized editor used for Source/Live Edit 2443 * within the CSS panel. 2444 */ 2445 function StyleSheetEditor(doc) 2446 { 2447 this.box = this.tag.replace({}, doc, this); 2448 this.input = this.box.firstChild; 2449 } 2450 2451 StyleSheetEditor.prototype = domplate(Firebug.BaseEditor, 2452 { 2453 multiLine: true, 2454 2455 tag: DIV( 2456 TEXTAREA({"class": "styleSheetEditor fullPanelEditor", oninput: "$onInput"}) 2457 ), 2458 2459 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 2460 2461 getValue: function() 2462 { 2463 return this.input.value; 2464 }, 2465 2466 setValue: function(value) 2467 { 2468 return this.input.value = value; 2469 }, 2470 2471 show: function(target, panel, value, textSize) 2472 { 2473 this.target = target; 2474 this.panel = panel; 2475 2476 this.panel.panelNode.appendChild(this.box); 2477 2478 this.input.value = value; 2479 this.input.focus(); 2480 2481 // match CSSModule.getEditorOptionKey 2482 var command = Firebug.chrome.$("cmd_firebug_togglecssEditMode"); 2483 command.setAttribute("checked", true); 2484 }, 2485 2486 hide: function() 2487 { 2488 var command = Firebug.chrome.$("cmd_firebug_togglecssEditMode"); 2489 command.setAttribute("checked", false); 2490 2491 if (this.box.parentNode == this.panel.panelNode) 2492 this.panel.panelNode.removeChild(this.box); 2493 2494 delete this.target; 2495 delete this.panel; 2496 delete this.styleSheet; 2497 }, 2498 2499 saveEdit: function(target, value, previousValue) 2500 { 2501 if (FBTrace.DBG_CSS) 2502 FBTrace.sysout("StyleSheetEditor.saveEdit", arguments); 2503 2504 CSSModule.freeEdit(this.styleSheet, value); 2505 }, 2506 2507 beginEditing: function() 2508 { 2509 if (FBTrace.DBG_CSS) 2510 FBTrace.sysout("StyleSheetEditor.beginEditing", arguments); 2511 2512 this.editing = true; 2513 }, 2514 2515 endEditing: function() 2516 { 2517 if (FBTrace.DBG_CSS) 2518 FBTrace.sysout("StyleSheetEditor.endEditing", arguments); 2519 2520 this.editing = false; 2521 this.panel.refresh(); 2522 return true; 2523 }, 2524 2525 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 2526 2527 onInput: function() 2528 { 2529 Firebug.Editor.update(); 2530 }, 2531 2532 scrollToLine: function(line, offset) 2533 { 2534 this.startMeasuring(this.input); 2535 var lineHeight = this.measureText().height; 2536 this.stopMeasuring(); 2537 2538 this.input.scrollTop = (line * lineHeight) + offset; 2539 } 2540 }); 2541 2542 Firebug.StyleSheetEditor = StyleSheetEditor; 2543 2544 // ********************************************************************************************* // 2545 2546 Firebug.CSSDirtyListener = function(context) 2547 { 2548 } 2549 2550 Firebug.CSSDirtyListener.isDirty = function(styleSheet, context) 2551 { 2552 return (styleSheet.fbDirty == true); 2553 } 2554 2555 Firebug.CSSDirtyListener.prototype = 2556 { 2557 markSheetDirty: function(styleSheet) 2558 { 2559 if (!styleSheet && FBTrace.DBG_ERRORS) 2560 { 2561 FBTrace.sysout("css; CSSDirtyListener markSheetDirty; styleSheet == NULL"); 2562 return; 2563 } 2564 2565 styleSheet.fbDirty = true; 2566 2567 if (FBTrace.DBG_CSS) 2568 FBTrace.sysout("CSSDirtyListener markSheetDirty " + styleSheet.href, styleSheet); 2569 }, 2570 2571 onCSSInsertRule: function(styleSheet, cssText, ruleIndex) 2572 { 2573 this.markSheetDirty(styleSheet); 2574 }, 2575 2576 onCSSDeleteRule: function(styleSheet, ruleIndex) 2577 { 2578 this.markSheetDirty(styleSheet); 2579 }, 2580 2581 onCSSSetProperty: function(style, propName, propValue, propPriority, prevValue, 2582 prevPriority, rule, baseText) 2583 { 2584 var styleSheet = rule.parentStyleSheet; 2585 if (styleSheet) 2586 this.markSheetDirty(styleSheet); 2587 }, 2588 2589 onCSSRemoveProperty: function(style, propName, prevValue, prevPriority, rule, baseText) 2590 { 2591 var styleSheet = rule.parentStyleSheet; 2592 if (styleSheet) 2593 this.markSheetDirty(styleSheet); 2594 } 2595 }; 2596 2597 // ********************************************************************************************* // 2598 // Local Helpers 2599 2600 function parsePriority(value) 2601 { 2602 var rePriority = /(.*?)\s*(!important)?$/; 2603 var m = rePriority.exec(value); 2604 var propValue = m ? m[1] : ""; 2605 var priority = m && m[2] ? "important" : ""; 2606 return {value: propValue, priority: priority}; 2607 } 2608 2609 function formatColor(color) 2610 { 2611 var colorDisplay = Options.get("colorDisplay"); 2612 2613 switch (colorDisplay) 2614 { 2615 case "hex": 2616 return Css.rgbToHex(color); 2617 2618 case "hsl": 2619 return Css.rgbToHSL(color); 2620 2621 default: 2622 return color; 2623 } 2624 } 2625 2626 function getRuleLine(rule) 2627 { 2628 // TODO return closest guess if rule isn't CSSStyleRule 2629 // and keep track of edited rule lines 2630 try 2631 { 2632 return Dom.domUtils.getRuleLine(rule); 2633 } 2634 catch (e) {} 2635 return 0; 2636 } 2637 2638 function getOriginalStyleSheetCSS(sheet, context) 2639 { 2640 if (sheet.ownerNode instanceof window.HTMLStyleElement) 2641 { 2642 return sheet.ownerNode.innerHTML; 2643 } 2644 else 2645 { 2646 // In the case, that there are no rules, the cache will return a message 2647 // to reload the source (see issue 4251) 2648 return sheet.cssRules.length != 0 ? context.sourceCache.load(sheet.href).join("") : ""; 2649 } 2650 } 2651 2652 function getStyleSheetCSS(sheet, context) 2653 { 2654 function beautify(css, indent) 2655 { 2656 var indent='\n'+Array(indent+1).join(' '); 2657 var i=css.indexOf('{'); 2658 var match=css.substr(i+1).match(/(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g); 2659 match.pop(); 2660 match.pop(); 2661 return css.substring(0, i+1) + indent 2662 + match.sort().join(indent) + '\n}'; 2663 } 2664 2665 var cssRules = sheet.cssRules, css=[]; 2666 for(var i = 0; i < cssRules.length; i++) 2667 { 2668 var rule = cssRules[i]; 2669 if (rule instanceof window.CSSStyleRule) 2670 css.push(beautify(rule.cssText, 4)); 2671 else 2672 css.push(rule.cssText); 2673 } 2674 2675 return Css.rgbToHex(css.join('\n\n')) + '\n'; 2676 } 2677 2678 function scrollSelectionIntoView(panel) 2679 { 2680 var selCon = getSelectionController(panel); 2681 selCon.scrollSelectionIntoView( 2682 Ci.nsISelectionController.SELECTION_NORMAL, 2683 Ci.nsISelectionController.SELECTION_FOCUS_REGION, true); 2684 } 2685 2686 function getSelectionController(panel) 2687 { 2688 var browser = Firebug.chrome.getPanelBrowser(panel); 2689 return browser.docShell.QueryInterface(Ci.nsIInterfaceRequestor) 2690 .getInterface(Ci.nsISelectionDisplay) 2691 .QueryInterface(Ci.nsISelectionController); 2692 } 2693 2694 // ********************************************************************************************* // 2695 // Registration 2696 2697 Firebug.registerPanel(Firebug.CSSStyleSheetPanel); 2698 2699 return Firebug.CSSStyleSheetPanel; 2700 2701 // ********************************************************************************************* // 2702 }}); 2703