Reviewed by Adam.
[WebKit-https.git] / WebCore / page / inspector / StylesSidebarPane.js
1 /*
2  * Copyright (C) 2007 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 WebInspector.StylesSidebarPane = function()
30 {
31     WebInspector.SidebarPane.call(this, "Styles");
32 }
33
34 WebInspector.StylesSidebarPane.prototype = {
35     update: function(node)
36     {
37         var body = this.bodyElement;
38
39         body.removeChildren();
40
41         this.sections = [];
42
43         if (!node)
44             return;
45
46         if (node.nodeType === Node.TEXT_NODE && node.parentNode)
47             node = node.parentNode;
48
49         var styleRules = [];
50         var styleProperties = [];
51
52         if (node.nodeType === Node.ELEMENT_NODE) {
53             var propertyCount = [];
54
55             var computedStyle = node.ownerDocument.defaultView.getComputedStyle(node);
56             if (computedStyle && computedStyle.length)
57                 styleRules.push({ computedStyle: true, selectorText: "Computed Style", style: computedStyle });
58
59             var nodeName = node.nodeName.toLowerCase();
60             for (var i = 0; i < node.attributes.length; ++i) {
61                 var attr = node.attributes[i];
62                 if (attr.style) {
63                     var attrStyle = { style: attr.style };
64                     attrStyle.subtitle = "element\u2019s \u201C" + attr.name + "\u201D attribute";
65                     attrStyle.selectorText = nodeName + "[" + attr.name;
66                     if (attr.value.length)
67                         attrStyle.selectorText += "=" + attr.value;
68                     attrStyle.selectorText += "]";
69                     styleRules.push(attrStyle);
70                 }
71             }
72
73             if (node.style && node.style.length) {
74                 var inlineStyle = { selectorText: "Inline Style Attribute", style: node.style };
75                 inlineStyle.subtitle = "element\u2019s \u201Cstyle\u201D attribute";
76                 styleRules.push(inlineStyle);
77             }
78
79             var matchedStyleRules = node.ownerDocument.defaultView.getMatchedCSSRules(node, "", !Preferences.showUserAgentStyles);
80             if (matchedStyleRules) {
81                 // Add rules in reverse order to match the cascade order.
82                 for (var i = (matchedStyleRules.length - 1); i >= 0; --i)
83                     styleRules.push(matchedStyleRules[i]);
84             }
85
86             var usedProperties = {};
87             var priorityUsed = false;
88
89             // Walk the style rules and make a list of all used and overloaded properties.
90             for (var i = 0; i < styleRules.length; ++i) {
91                 if (styleRules[i].computedStyle)
92                     continue;
93
94                 styleRules[i].overloadedProperties = {};
95
96                 var foundProperties = {};
97
98                 var style = styleRules[i].style;
99                 for (var j = 0; j < style.length; ++j) {
100                     var name = style[j];
101                     var shorthand = style.getPropertyShorthand(name);
102                     var overloaded = (name in usedProperties);
103
104                     if (!priorityUsed && style.getPropertyPriority(name).length)
105                         priorityUsed = true;
106
107                     if (overloaded)
108                         styleRules[i].overloadedProperties[name] = true;
109
110                     foundProperties[name] = true;
111                     if (shorthand)
112                         foundProperties[shorthand] = true;
113
114                     if (name === "font") {
115                         // The font property is not reported as a shorthand. Report finding the individual
116                         // properties so they are visible in computed style.
117                         // FIXME: remove this when http://bugs.webkit.org/show_bug.cgi?id=15598 is fixed.
118                         foundProperties["font-family"] = true;
119                         foundProperties["font-size"] = true;
120                         foundProperties["font-style"] = true;
121                         foundProperties["font-variant"] = true;
122                         foundProperties["font-weight"] = true;
123                         foundProperties["line-height"] = true;
124                     }
125                 }
126
127                 // Add all the properties found in this style to the used properties list.
128                 // Do this here so only future rules are affect by properties used in this rule.
129                 for (var name in foundProperties)
130                     usedProperties[name] = true;
131             }
132
133             if (priorityUsed) {
134                 // Walk the properties again and account for !important.
135                 var foundPriorityProperties = [];
136
137                 // Walk in reverse to match the order !important overrides.
138                 for (var i = (styleRules.length - 1); i >= 0; --i) {
139                     if (styleRules[i].computedStyle)
140                         continue;
141
142                     var foundProperties = {};
143                     var style = styleRules[i].style;
144                     for (var j = 0; j < style.length; ++j) {
145                         var name = style[j];
146
147                         // Skip duplicate properties in the same rule.
148                         if (name in foundProperties)
149                             continue;
150
151                         foundProperties[name] = true;
152
153                         if (style.getPropertyPriority(name).length) {
154                             if (!(name in foundPriorityProperties))
155                                 delete styleRules[i].overloadedProperties[name];
156                             else
157                                 styleRules[i].overloadedProperties[name] = true;
158                             foundPriorityProperties[name] = true;
159                         } else if (name in foundPriorityProperties)
160                             styleRules[i].overloadedProperties[name] = true;
161                     }
162                 }
163             }
164
165             // Make a property section for each style rule.
166             var styleRulesLength = styleRules.length;
167             for (var i = 0; i < styleRulesLength; ++i) {
168                 var styleRule = styleRules[i];
169                 var subtitle = styleRule.subtitle;
170                 delete styleRule.subtitle;
171
172                 var computedStyle = styleRule.computedStyle;
173                 delete styleRule.computedStyle;
174
175                 var overloadedProperties = styleRule.overloadedProperties;
176                 delete styleRule.overloadedProperties;
177
178                 var section = new WebInspector.StylePropertiesSection(styleRule, subtitle, computedStyle, (overloadedProperties || usedProperties));
179                 section.expanded = true;
180
181                 body.appendChild(section.element);
182                 this.sections.push(section);
183             }
184         } else {
185             // can't style this node
186         }
187     }
188 }
189
190 WebInspector.StylesSidebarPane.prototype.__proto__ = WebInspector.SidebarPane.prototype;
191
192 WebInspector.StylePropertiesSection = function(styleRule, subtitle, computedStyle, overloadedOrUsedProperties)
193 {
194     WebInspector.PropertiesSection.call(this, styleRule.selectorText);
195
196     this.styleRule = styleRule;
197     this.computedStyle = computedStyle;
198
199     if (computedStyle)
200         this.usedProperties = overloadedOrUsedProperties;
201     else
202         this.overloadedProperties = overloadedOrUsedProperties || {};
203
204     if (computedStyle) {
205         if (Preferences.showInheritedComputedStyleProperties)
206             this.element.addStyleClass("show-inherited");
207
208         var showInheritedLabel = document.createElement("label");
209         var showInheritedInput = document.createElement("input");
210         showInheritedInput.type = "checkbox";
211         showInheritedInput.checked = Preferences.showInheritedComputedStyleProperties;
212
213         var computedStyleSection = this;
214         var showInheritedToggleFunction = function(event) {
215             Preferences.showInheritedComputedStyleProperties = showInheritedInput.checked;
216             if (Preferences.showInheritedComputedStyleProperties)
217                 computedStyleSection.element.addStyleClass("show-inherited");
218             else
219                 computedStyleSection.element.removeStyleClass("show-inherited");
220             event.stopPropagation();
221         };
222
223         showInheritedLabel.addEventListener("click", showInheritedToggleFunction, false);
224
225         showInheritedLabel.appendChild(showInheritedInput);
226         showInheritedLabel.appendChild(document.createTextNode("Show inherited properties"));
227         this.subtitleElement.appendChild(showInheritedLabel);
228     } else {
229         if (!subtitle) {
230             if (this.styleRule.parentStyleSheet && this.styleRule.parentStyleSheet.href) {
231                 var url = this.styleRule.parentStyleSheet.href;
232                 subtitle = WebInspector.linkifyURL(url, url.trimURL(WebInspector.mainResource.domain).escapeHTML());
233                 this.subtitleElement.addStyleClass("file");
234             } else if (this.styleRule.parentStyleSheet && !this.styleRule.parentStyleSheet.ownerNode)
235                 subtitle = "user agent stylesheet";
236             else
237                 subtitle = "inline stylesheet";
238         }
239
240         this.subtitle = subtitle;
241     }
242 }
243
244 WebInspector.StylePropertiesSection.prototype = {
245     onpopulate: function()
246     {
247         var style = this.styleRule.style;
248         if (!style.length)
249             return;
250
251         var foundProperties = {};
252
253         // Add properties in reverse order to better match how the style
254         // system picks the winning value for duplicate properties.
255         for (var i = (style.length - 1); i >= 0; --i) {
256             var name = style[i];
257             var shorthand = style.getPropertyShorthand(name);
258
259             if (name in foundProperties || (shorthand && shorthand in foundProperties))
260                 continue;
261
262             foundProperties[name] = true;
263             if (shorthand)
264                 foundProperties[shorthand] = true;
265
266             if (this.computedStyle)
267                 var inherited = (this.usedProperties && !((shorthand || name) in this.usedProperties));
268             else {
269                 var overloaded = ((shorthand || name) in this.overloadedProperties);
270
271                 if (shorthand && !overloaded) {
272                     // Find out if all the individual properties of a shorthand
273                     // are overloaded and mark the shorthand as overloaded too.
274
275                     var count = 0;
276                     var overloadCount = 0;
277                     for (var j = 0; j < style.length; ++j) {
278                         var individualProperty = style[j];
279                         if (style.getPropertyShorthand(individualProperty) !== shorthand)
280                             continue;
281                         ++count;
282                         if (individualProperty in this.overloadedProperties)
283                             ++overloadCount;
284                     }
285
286                     overloaded = (overloadCount >= count);
287                 }
288             }
289
290             var item = new WebInspector.StylePropertyTreeElement(style, (shorthand || name), this.computedStyle, (shorthand ? true : false), (overloaded || inherited));
291             this.propertiesTreeOutline.insertChild(item, 0);
292         }
293     }
294 }
295
296 WebInspector.StylePropertiesSection.prototype.__proto__ = WebInspector.PropertiesSection.prototype;
297
298 WebInspector.StylePropertyTreeElement = function(style, name, computedStyle, shorthand, overloadedOrInherited)
299 {
300     // These properties should always show for Computed Style
301     var alwaysShowComputedProperties = { "display": true, "height": true, "width": true };
302
303     // "Nicknames" for some common values that are easier to read.
304     var valueNicknames = {
305         "rgb(0, 0, 0)": "black",
306         "#000": "black",
307         "#000000": "black",
308         "rgb(255, 255, 255)": "white",
309         "#fff": "white",
310         "#ffffff": "white",
311         "#FFF": "white",
312         "#FFFFFF": "white",
313         "rgba(0, 0, 0, 0)": "transparent",
314         "rgb(255, 0, 0)": "red",
315         "rgb(0, 255, 0)": "lime",
316         "rgb(0, 0, 255)": "blue",
317         "rgb(255, 255, 0)": "yellow",
318         "rgb(255, 0, 255)": "magenta",
319         "rgb(0, 255, 255)": "cyan"
320     };
321
322     this.style = style;
323     this.name = name;
324     this.computedStyle = computedStyle;
325     this.shorthand = shorthand;
326     this.overloaded = (!computedStyle && overloadedOrInherited);
327     this.inherited = (computedStyle && overloadedOrInherited && !(name in alwaysShowComputedProperties));
328
329     var priority = style.getPropertyPriority(name);
330     var value = style.getPropertyValue(name);
331     var htmlValue = value;
332
333     if (priority && !priority.length)
334         delete priority;
335
336     if (!priority && shorthand) {
337         // Priority is not returned for shorthands, find the priority from an individual property.
338         for (var i = 0; i < style.length; ++i) {
339             var individualProperty = style[i];
340             if (style.getPropertyShorthand(individualProperty) !== name)
341                 continue;
342             priority = style.getPropertyPriority(individualProperty);
343             break;
344         }
345     }
346
347     if (value) {
348         var urls = value.match(/url\([^)]+\)/);
349         if (urls) {
350             for (var i = 0; i < urls.length; ++i) {
351                 var url = urls[i].substring(4, urls[i].length - 1);
352                 htmlValue = htmlValue.replace(urls[i], "url(" + WebInspector.linkifyURL(url) + ")");
353             }
354         } else {
355             if (value in valueNicknames)
356                 htmlValue = valueNicknames[value];
357             htmlValue = htmlValue.escapeHTML();
358         }
359     } else if (shorthand) {
360         // Some shorthands (like border) return a null value, so compute a shorthand value.
361         // FIXME: remove this when http://bugs.webkit.org/show_bug.cgi?id=15823 is fixed.
362
363         value = "";
364
365         var foundProperties = {};
366         for (var i = 0; i < style.length; ++i) {
367             var individualProperty = style[i];
368             if (style.getPropertyShorthand(individualProperty) !== name || individualProperty in foundProperties)
369                 continue;
370
371             var individualValue = style.getPropertyValue(individualProperty);
372             if (style.isPropertyImplicit(individualProperty) || individualValue === "initial")
373                 continue;
374
375             foundProperties[individualProperty] = true;
376
377             if (value.length)
378                 value += " ";
379             value += individualValue;
380         }
381
382         htmlValue = value.escapeHTML();
383     } else
384         htmlValue = value = "";
385
386     var classes = [];
387     if (!computedStyle && (style.isPropertyImplicit(name) || value === "initial"))
388         classes.push("implicit");
389     if (this.inherited)
390         classes.push("inherited");
391     if (this.overloaded)
392         classes.push("overloaded");
393
394     var title = "";
395     if (classes.length)
396         title += "<span class=\"" + classes.join(" ") + "\">";
397
398     title += "<span class=\"name\">" + name.escapeHTML() + "</span>: ";
399     title += "<span class=\"value\">" + htmlValue;
400     if (priority)
401         title += " !" + priority;
402     title += "</span>;";
403
404     if (value) {
405         // FIXME: this dosen't catch keyword based colors like black and white
406         var colors = value.match(/((rgb|hsl)a?\([^)]+\))|(#[0-9a-fA-F]{6})|(#[0-9a-fA-F]{3})/g);
407         if (colors) {
408             var colorsLength = colors.length;
409             for (var i = 0; i < colorsLength; ++i)
410                 title += "<span class=\"swatch\" style=\"background-color: " + colors[i] + "\"></span>";
411         }
412     }
413
414     if (classes.length)
415         title += "</span>";
416
417     TreeElement.call(this, title, null, shorthand);
418
419     this.tooltip = name + ": " + (valueNicknames[value] || value) + (priority ? " !" + priority : "");
420 }
421
422 WebInspector.StylePropertyTreeElement.prototype = {
423     onpopulate: function()
424     {
425         // Only populate once and if this property is a shorthand.
426         if (this.children.length || !this.shorthand)
427             return;
428
429         var foundProperties = {};
430
431         // Add properties in reverse order to better match how the style
432         // system picks the winning value for duplicate properties.
433         for (var i = (this.style.length - 1); i >= 0; --i) {
434             var name = this.style[i];
435             var shorthand = this.style.getPropertyShorthand(name);
436
437             if (shorthand !== this.name || name in foundProperties)
438                 continue;
439
440             foundProperties[name] = true;
441
442             if (this.computedStyle)
443                 var inherited = (this.treeOutline.section.usedProperties && !(name in this.treeOutline.section.usedProperties));
444             else
445                 var overloaded = (name in this.treeOutline.section.overloadedProperties);
446
447             var item = new WebInspector.StylePropertyTreeElement(this.style, name, this.computedStyle, false, (inherited || overloaded));
448             this.insertChild(item, 0);
449         }
450     }
451 }
452
453 WebInspector.StylePropertyTreeElement.prototype.__proto__ = TreeElement.prototype;