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