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