WebKitTools:
[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, editedSection)
36     {
37         var refresh = false;
38
39         if (!node || node === this.node)
40             refresh = true;
41
42         if (node && node.nodeType === Node.TEXT_NODE && node.parentNode)
43             node = node.parentNode;
44
45         if (node && node.nodeType !== Node.ELEMENT_NODE)
46             node = null;
47
48         if (node)
49             this.node = node;
50         else
51             node = this.node;
52
53         var body = this.bodyElement;
54         if (!refresh || !node) {
55             body.removeChildren();
56             this.sections = [];
57         }
58
59         if (!node)
60             return;
61
62         var styleRules = [];
63
64         if (refresh) {
65             for (var i = 0; i < this.sections.length; ++i) {
66                 var section = this.sections[i];
67                 if (section.computedStyle)
68                     section.styleRule.style = node.ownerDocument.defaultView.getComputedStyle(node);
69                 var styleRule = { section: section, style: section.styleRule.style, computedStyle: section.computedStyle };
70                 styleRules.push(styleRule);
71             }
72         } else {
73             var computedStyle = node.ownerDocument.defaultView.getComputedStyle(node);
74             styleRules.push({ computedStyle: true, selectorText: "Computed Style", style: computedStyle, editable: false });
75
76             var nodeName = node.nodeName.toLowerCase();
77             for (var i = 0; i < node.attributes.length; ++i) {
78                 var attr = node.attributes[i];
79                 if (attr.style) {
80                     var attrStyle = { style: attr.style, editable: false };
81                     attrStyle.subtitle = "element\u2019s \u201C" + attr.name + "\u201D attribute";
82                     attrStyle.selectorText = nodeName + "[" + attr.name;
83                     if (attr.value.length)
84                         attrStyle.selectorText += "=" + attr.value;
85                     attrStyle.selectorText += "]";
86                     styleRules.push(attrStyle);
87                 }
88             }
89
90             if (node.style && node.style.length) {
91                 var inlineStyle = { selectorText: "Inline Style Attribute", style: node.style };
92                 inlineStyle.subtitle = "element\u2019s \u201Cstyle\u201D attribute";
93                 styleRules.push(inlineStyle);
94             }
95
96             var matchedStyleRules = node.ownerDocument.defaultView.getMatchedCSSRules(node, "", !Preferences.showUserAgentStyles);
97             if (matchedStyleRules) {
98                 // Add rules in reverse order to match the cascade order.
99                 for (var i = (matchedStyleRules.length - 1); i >= 0; --i)
100                     styleRules.push(matchedStyleRules[i]);
101             }
102         }
103
104         var usedProperties = {};
105         var priorityUsed = false;
106
107         // Walk the style rules and make a list of all used and overloaded properties.
108         for (var i = 0; i < styleRules.length; ++i) {
109             var styleRule = styleRules[i];
110             if (styleRule.computedStyle)
111                 continue;
112
113             styleRule.usedProperties = {};
114
115             var style = styleRule.style;
116             for (var j = 0; j < style.length; ++j) {
117                 var name = style[j];
118
119                 if (!priorityUsed && style.getPropertyPriority(name).length)
120                     priorityUsed = true;
121
122                 // If the property name is already used by another rule this is rule's
123                 // property is overloaded, so don't add it to the rule's usedProperties.
124                 if (!(name in usedProperties))
125                     styleRule.usedProperties[name] = true;
126
127                 if (name === "font") {
128                     // The font property is not reported as a shorthand. Report finding the individual
129                     // properties so they are visible in computed style.
130                     // FIXME: remove this when http://bugs.webkit.org/show_bug.cgi?id=15598 is fixed.
131                     styleRule.usedProperties["font-family"] = true;
132                     styleRule.usedProperties["font-size"] = true;
133                     styleRule.usedProperties["font-style"] = true;
134                     styleRule.usedProperties["font-variant"] = true;
135                     styleRule.usedProperties["font-weight"] = true;
136                     styleRule.usedProperties["line-height"] = true;
137                 }
138             }
139
140             // Add all the properties found in this style to the used properties list.
141             // Do this here so only future rules are affect by properties used in this rule.
142             for (var name in styleRules[i].usedProperties)
143                 usedProperties[name] = true;
144         }
145
146         if (priorityUsed) {
147             // Walk the properties again and account for !important.
148             var foundPriorityProperties = [];
149
150             // Walk in reverse to match the order !important overrides.
151             for (var i = (styleRules.length - 1); i >= 0; --i) {
152                 if (styleRules[i].computedStyle)
153                     continue;
154
155                 var foundProperties = {};
156                 var style = styleRules[i].style;
157                 for (var j = 0; j < style.length; ++j) {
158                     var name = style[j];
159
160                     // Skip duplicate properties in the same rule.
161                     if (name in foundProperties)
162                         continue;
163
164                     foundProperties[name] = true;
165
166                     if (style.getPropertyPriority(name).length) {
167                         if (!(name in foundPriorityProperties))
168                             styleRules[i].usedProperties[name] = true;
169                         else
170                             delete styleRules[i].usedProperties[name];
171                         foundPriorityProperties[name] = true;
172                     } else if (name in foundPriorityProperties)
173                         delete styleRules[i].usedProperties[name];
174                 }
175             }
176         }
177
178         if (refresh) {
179             // Walk the style rules and update the sections with new overloaded and used properties.
180             for (var i = 0; i < styleRules.length; ++i) {
181                 var styleRule = styleRules[i];
182                 var section = styleRule.section;
183                 section._usedProperties = (styleRule.usedProperties || usedProperties);
184                 section.update((section === editedSection) || styleRule.computedStyle);
185             }
186         } else {
187             // Make a property section for each style rule.
188             for (var i = 0; i < styleRules.length; ++i) {
189                 var styleRule = styleRules[i];
190                 var subtitle = styleRule.subtitle;
191                 delete styleRule.subtitle;
192
193                 var computedStyle = styleRule.computedStyle;
194                 delete styleRule.computedStyle;
195
196                 var ruleUsedProperties = styleRule.usedProperties;
197                 delete styleRule.usedProperties;
198
199                 var editable = styleRule.editable;
200                 delete styleRule.editable;
201
202                 // Default editable to true if it was omitted.
203                 if (typeof editable === "undefined")
204                     editable = true;
205
206                 var section = new WebInspector.StylePropertiesSection(styleRule, subtitle, computedStyle, (ruleUsedProperties || usedProperties), editable);
207                 section.expanded = true;
208                 section.pane = this;
209
210                 body.appendChild(section.element);
211                 this.sections.push(section);
212             }
213         }
214     }
215 }
216
217 WebInspector.StylesSidebarPane.prototype.__proto__ = WebInspector.SidebarPane.prototype;
218
219 WebInspector.StylePropertiesSection = function(styleRule, subtitle, computedStyle, usedProperties, editable)
220 {
221     WebInspector.PropertiesSection.call(this, styleRule.selectorText);
222
223     this.styleRule = styleRule;
224     this.computedStyle = computedStyle;
225     this.editable = (editable && !computedStyle);
226
227     // Prevent editing the user agent rules.
228     if (this.styleRule.parentStyleSheet && !this.styleRule.parentStyleSheet.ownerNode)
229         this.editable = false;
230
231     this._usedProperties = usedProperties;
232
233     if (computedStyle) {
234         if (Preferences.showInheritedComputedStyleProperties)
235             this.element.addStyleClass("show-inherited");
236
237         var showInheritedLabel = document.createElement("label");
238         var showInheritedInput = document.createElement("input");
239         showInheritedInput.type = "checkbox";
240         showInheritedInput.checked = Preferences.showInheritedComputedStyleProperties;
241
242         var computedStyleSection = this;
243         var showInheritedToggleFunction = function(event) {
244             Preferences.showInheritedComputedStyleProperties = showInheritedInput.checked;
245             if (Preferences.showInheritedComputedStyleProperties)
246                 computedStyleSection.element.addStyleClass("show-inherited");
247             else
248                 computedStyleSection.element.removeStyleClass("show-inherited");
249             event.stopPropagation();
250         };
251
252         showInheritedLabel.addEventListener("click", showInheritedToggleFunction, false);
253
254         showInheritedLabel.appendChild(showInheritedInput);
255         showInheritedLabel.appendChild(document.createTextNode("Show inherited properties"));
256         this.subtitleElement.appendChild(showInheritedLabel);
257     } else {
258         if (!subtitle) {
259             if (this.styleRule.parentStyleSheet && this.styleRule.parentStyleSheet.href) {
260                 var url = this.styleRule.parentStyleSheet.href;
261                 subtitle = WebInspector.linkifyURL(url, url.trimURL(WebInspector.mainResource.domain).escapeHTML());
262                 this.subtitleElement.addStyleClass("file");
263             } else if (this.styleRule.parentStyleSheet && !this.styleRule.parentStyleSheet.ownerNode)
264                 subtitle = "user agent stylesheet";
265             else
266                 subtitle = "inline stylesheet";
267         }
268
269         this.subtitle = subtitle;
270     }
271 }
272
273 WebInspector.StylePropertiesSection.prototype = {
274     get usedProperties()
275     {
276         return this._usedProperties || {};
277     },
278
279     set usedProperties(x)
280     {
281         this._usedProperties = x;
282         this.update();
283     },
284
285     isPropertyInherited: function(property)
286     {
287         if (!this.computedStyle || !this._usedProperties)
288             return false;
289         // These properties should always show for Computed Style.
290         var alwaysShowComputedProperties = { "display": true, "height": true, "width": true };
291         return !(property in this.usedProperties) && !(property in alwaysShowComputedProperties);
292     },
293
294     isPropertyOverloaded: function(property, shorthand)
295     {
296         if (this.computedStyle || !this._usedProperties)
297             return false;
298
299         var used = (property in this.usedProperties);
300         if (used || !shorthand)
301             return !used;
302
303         // Find out if any of the individual longhand properties of the shorthand
304         // are used, if none are then the shorthand is overloaded too.
305         var longhandProperties = this.styleRule.style.getLonghandProperties(property);
306         for (var j = 0; j < longhandProperties.length; ++j) {
307             var individualProperty = longhandProperties[j];
308             if (individualProperty in this.usedProperties)
309                 return false;
310         }
311
312         return true;
313     },
314
315     update: function(full)
316     {
317         if (full || this.computedStyle) {
318             this.propertiesTreeOutline.removeChildren();
319             this.populated = false;
320         } else {
321             var child = this.propertiesTreeOutline.children[0];
322             while (child) {
323                 child.overloaded = this.isPropertyOverloaded(child.name, child.shorthand);
324                 child = child.traverseNextTreeElement(false, null, true);
325             }
326         }
327     },
328
329     onpopulate: function()
330     {
331         var style = this.styleRule.style;
332         if (!style.length)
333             return;
334
335         var foundShorthands = {};
336         var uniqueProperties = style.getUniqueProperties();
337         uniqueProperties.sort();
338
339         for (var i = 0; i < uniqueProperties.length; ++i) {
340             var name = uniqueProperties[i];
341             var shorthand = style.getPropertyShorthand(name);
342
343             if (shorthand && shorthand in foundShorthands)
344                 continue;
345
346             if (shorthand) {
347                 foundShorthands[shorthand] = true;
348                 name = shorthand;
349             }
350
351             var isShorthand = (shorthand ? true : false);
352             var inherited = this.isPropertyInherited(name);
353             var overloaded = this.isPropertyOverloaded(name, isShorthand);
354
355             var item = new WebInspector.StylePropertyTreeElement(style, name, isShorthand, inherited, overloaded);
356             this.propertiesTreeOutline.appendChild(item);
357         }
358     }
359 }
360
361 WebInspector.StylePropertiesSection.prototype.__proto__ = WebInspector.PropertiesSection.prototype;
362
363 WebInspector.StylePropertyTreeElement = function(style, name, shorthand, inherited, overloaded)
364 {
365     this.style = style;
366     this.name = name;
367     this.shorthand = shorthand;
368     this._inherited = inherited;
369     this._overloaded = overloaded;
370
371     // Pass an empty title, the title gets made later in onattach.
372     TreeElement.call(this, "", null, shorthand);
373 }
374
375 WebInspector.StylePropertyTreeElement.prototype = {
376     get inherited()
377     {
378         return this._inherited;
379     },
380
381     set inherited(x)
382     {
383         if (x === this._inherited)
384             return;
385         this._inherited = x;
386         this.updateState();
387     },
388
389     get overloaded()
390     {
391         return this._overloaded;
392     },
393
394     set overloaded(x)
395     {
396         if (x === this._overloaded)
397             return;
398         this._overloaded = x;
399         this.updateState();
400     },
401
402     onattach: function()
403     {
404         this.updateTitle();
405     },
406
407     updateTitle: function()
408     {
409         // "Nicknames" for some common values that are easier to read.
410         var valueNicknames = {
411             "rgb(0, 0, 0)": "black",
412             "#000": "black",
413             "#000000": "black",
414             "rgb(255, 255, 255)": "white",
415             "#fff": "white",
416             "#ffffff": "white",
417             "#FFF": "white",
418             "#FFFFFF": "white",
419             "rgba(0, 0, 0, 0)": "transparent",
420             "rgb(255, 0, 0)": "red",
421             "rgb(0, 255, 0)": "lime",
422             "rgb(0, 0, 255)": "blue",
423             "rgb(255, 255, 0)": "yellow",
424             "rgb(255, 0, 255)": "magenta",
425             "rgb(0, 255, 255)": "cyan"
426         };
427
428         var priority = (this.shorthand ? this.style.getShorthandPriority(this.name) : this.style.getPropertyPriority(this.name));
429         var value = (this.shorthand ? this.style.getShorthandValue(this.name) : this.style.getPropertyValue(this.name));
430         var htmlValue = value;
431
432         if (priority && !priority.length)
433             delete priority;
434         if (priority)
435             priority = "!" + priority;
436
437         if (value) {
438             var urls = value.match(/url\([^)]+\)/);
439             if (urls) {
440                 for (var i = 0; i < urls.length; ++i) {
441                     var url = urls[i].substring(4, urls[i].length - 1);
442                     htmlValue = htmlValue.replace(urls[i], "url(" + WebInspector.linkifyURL(url) + ")");
443                 }
444             } else {
445                 if (value in valueNicknames)
446                     htmlValue = valueNicknames[value];
447                 htmlValue = htmlValue.escapeHTML();
448             }
449         } else
450             htmlValue = value = "";
451
452         this.updateState();
453
454         var nameElement = document.createElement("span");
455         nameElement.className = "name";
456         nameElement.textContent = this.name;
457
458         var valueElement = document.createElement("span");
459         valueElement.className = "value";
460         valueElement.innerHTML = htmlValue;
461
462         if (priority) {
463             var priorityElement = document.createElement("span");
464             priorityElement.className = "priority";
465             priorityElement.textContent = priority;
466         }
467
468         this.listItemElement.removeChildren();
469
470         this.listItemElement.appendChild(nameElement);
471         this.listItemElement.appendChild(document.createTextNode(": "));
472         this.listItemElement.appendChild(valueElement);
473
474         if (priorityElement) {
475             this.listItemElement.appendChild(document.createTextNode(" "));
476             this.listItemElement.appendChild(priorityElement);
477         }
478
479         this.listItemElement.appendChild(document.createTextNode(";"));
480
481         if (value) {
482             // FIXME: this dosen't catch keyword based colors like black and white
483             var colors = value.match(/((rgb|hsl)a?\([^)]+\))|(#[0-9a-fA-F]{6})|(#[0-9a-fA-F]{3})/g);
484             if (colors) {
485                 var colorsLength = colors.length;
486                 for (var i = 0; i < colorsLength; ++i) {
487                     var swatchElement = document.createElement("span");
488                     swatchElement.className = "swatch";
489                     swatchElement.style.setProperty("background-color", colors[i]);
490                     this.listItemElement.appendChild(swatchElement);
491                 }
492             }
493         }
494
495         this.tooltip = this.name + ": " + (valueNicknames[value] || value) + (priority ? " " + priority : "");
496     },
497
498     updateState: function()
499     {
500         if (!this.listItemElement)
501             return;
502
503         var value = (this.shorthand ? this.style.getShorthandValue(this.name) : this.style.getPropertyValue(this.name));
504         if (this.style.isPropertyImplicit(this.name) || value === "initial")
505             this.listItemElement.addStyleClass("implicit");
506         else
507             this.listItemElement.removeStyleClass("implicit");
508
509         if (this.inherited)
510             this.listItemElement.addStyleClass("inherited");
511         else
512             this.listItemElement.removeStyleClass("inherited");
513
514         if (this.overloaded)
515             this.listItemElement.addStyleClass("overloaded");
516         else
517             this.listItemElement.removeStyleClass("overloaded");
518     },
519
520     onpopulate: function()
521     {
522         // Only populate once and if this property is a shorthand.
523         if (this.children.length || !this.shorthand)
524             return;
525
526         var longhandProperties = this.style.getLonghandProperties(this.name);
527         for (var i = 0; i < longhandProperties.length; ++i) {
528             var name = longhandProperties[i];
529
530             if (this.treeOutline.section) {
531                 var inherited = this.treeOutline.section.isPropertyInherited(name);
532                 var overloaded = this.treeOutline.section.isPropertyOverloaded(name);
533             }
534
535             var item = new WebInspector.StylePropertyTreeElement(this.style, name, false, inherited, overloaded);
536             this.appendChild(item);
537         }
538     },
539
540     ondblclick: function(element, event)
541     {
542         this.startEditing(event.target);
543     },
544
545     startEditing: function(selectElement)
546     {
547         // FIXME: we don't allow editing of longhand properties under a shorthand right now.
548         if (this.parent.shorthand)
549             return;
550
551         if (this.editing || (this.treeOutline.section && !this.treeOutline.section.editable))
552             return;
553
554         this.editing = true;
555         this.previousTextContent = this.listItemElement.textContent;
556
557         this.listItemElement.addStyleClass("focusable");
558         this.listItemElement.addStyleClass("editing");
559         this.wasExpanded = this.expanded;
560         this.collapse();
561         // Lie about out children to prevent toggling on click.
562         this.hasChildren = false;
563
564         if (!selectElement)
565             selectElement = this.listItemElement;
566
567         window.getSelection().setBaseAndExtent(selectElement, 0, selectElement, 1);
568
569         var treeElement = this;
570         this.listItemElement.blurred = function() { treeElement.commitEditing() };
571         this.listItemElement.handleKeyEvent = function(event) {
572             if (event.keyIdentifier === "Enter") {
573                 treeElement.commitEditing();
574                 event.preventDefault();
575             } else if (event.keyCode === 27) { // Escape key
576                 treeElement.cancelEditing();
577                 event.preventDefault();
578             }
579         };
580
581         this.previousFocusElement = WebInspector.currentFocusElement;
582         WebInspector.currentFocusElement = this.listItemElement;
583     },
584
585     endEditing: function()
586     {
587         // Revert the changes done in startEditing().
588         delete this.listItemElement.blurred;
589         delete this.listItemElement.handleKeyEvent;
590
591         WebInspector.currentFocusElement = this.previousFocusElement;
592         delete this.previousFocusElement;
593
594         delete this.previousTextContent;
595         delete this.editing;
596
597         this.listItemElement.removeStyleClass("focusable");
598         this.listItemElement.removeStyleClass("editing");
599         this.hasChildren = (this.children.length ? true : false);
600         if (this.wasExpanded) {
601             delete this.wasExpanded;
602             this.expand();
603         }
604     },
605
606     cancelEditing: function()
607     {
608         this.endEditing();
609         this.updateTitle();
610     },
611
612     commitEditing: function()
613     {
614         var previousContent = this.previousTextContent;
615
616         this.endEditing();
617
618         var userInput = this.listItemElement.textContent;
619         if (userInput === previousContent)
620             return; // nothing changed, so do nothing else
621
622         var userInputLength = userInput.trimWhitespace().length;
623
624         // Create a new element to parse the user input CSS.
625         var parseElement = document.createElement("span");
626         parseElement.setAttribute("style", userInput);
627
628         var userInputStyle = parseElement.style;
629         if (userInputStyle.length || !userInputLength) {
630             // The input was parsable or the user deleted everything, so remove the
631             // original property from the real style declaration. If this represents
632             // a shorthand remove all the longhand properties.
633             if (this.shorthand) {
634                 var longhandProperties = this.style.getLonghandProperties(this.name);
635                 for (var i = 0; i < longhandProperties.length; ++i)
636                     this.style.removeProperty(longhandProperties[i]);
637             } else
638                 this.style.removeProperty(this.name);
639         }
640
641         if (!userInputLength) {
642             // The user deleted the everything, so remove the tree element and update.
643             if (this.treeOutline.section && this.treeOutline.section.pane)
644                 this.treeOutline.section.pane.update();
645             this.parent.removeChild(this);
646             return;
647         }
648
649         if (!userInputStyle.length) {
650             // The user typed something, but it didn't parse. Just abort and restore
651             // the original title for this property.
652             this.updateTitle();
653             return;
654         }
655
656         // Iterate of the properties on the test element's style declaration and
657         // add them to the real style declaration. We take care to move shorthands.
658         var foundShorthands = {};
659         var uniqueProperties = userInputStyle.getUniqueProperties();
660         for (var i = 0; i < uniqueProperties.length; ++i) {
661             var name = uniqueProperties[i];
662             var shorthand = userInputStyle.getPropertyShorthand(name);
663
664             if (shorthand && shorthand in foundShorthands)
665                 continue;
666
667             if (shorthand) {
668                 var value = userInputStyle.getShorthandValue(shorthand);
669                 var priority = userInputStyle.getShorthandPriority(shorthand);
670                 foundShorthands[shorthand] = true;
671             } else {
672                 var value = userInputStyle.getPropertyValue(name);
673                 var priority = userInputStyle.getPropertyPriority(name);
674             }
675
676             // Set the property on the real style declaration.
677             this.style.setProperty((shorthand || name), value, priority);
678         }
679
680         if (this.treeOutline.section && this.treeOutline.section.pane)
681             this.treeOutline.section.pane.update(null, this.treeOutline.section);
682         else if (this.treeOutline.section)
683             this.treeOutline.section.update(true);
684         else
685             this.updateTitle(); // FIXME: this will not show new properties. But we don't hit his case yet.
686     }
687 }
688
689 WebInspector.StylePropertyTreeElement.prototype.__proto__ = TreeElement.prototype;