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