• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2007 Apple Inc.  All rights reserved.
3 * Copyright (C) 2009 Joseph Pecoraro
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1.  Redistributions of source code must retain the above copyright
10 *     notice, this list of conditions and the following disclaimer.
11 * 2.  Redistributions in binary form must reproduce the above copyright
12 *     notice, this list of conditions and the following disclaimer in the
13 *     documentation and/or other materials provided with the distribution.
14 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 *     its contributors may be used to endorse or promote products derived
16 *     from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30WebInspector.StylesSidebarPane = function()
31{
32    WebInspector.SidebarPane.call(this, WebInspector.UIString("Styles"));
33}
34
35WebInspector.StylesSidebarPane.prototype = {
36    update: function(node, editedSection, forceUpdate)
37    {
38        var refresh = false;
39
40        if (forceUpdate)
41            delete this.node;
42
43        if (!forceUpdate && (!node || node === this.node))
44            refresh = true;
45
46        if (node && node.nodeType === Node.TEXT_NODE && node.parentNode)
47            node = node.parentNode;
48
49        if (node && node.nodeType !== Node.ELEMENT_NODE)
50            node = null;
51
52        if (node)
53            this.node = node;
54        else
55            node = this.node;
56
57        var body = this.bodyElement;
58        if (!refresh || !node) {
59            body.removeChildren();
60            this.sections = [];
61        }
62
63        if (!node)
64            return;
65
66        var self = this;
67        var callback = function(styles) {
68            if (!styles)
69                return;
70            var nodeWrapper = WebInspector.wrapNodeWithStyles(node, styles);
71            self._update(refresh, body, nodeWrapper, editedSection, forceUpdate);
72        };
73        InspectorController.getStyles(node, !Preferences.showUserAgentStyles, callback);
74    },
75
76    _update: function(refresh, body, node, editedSection, forceUpdate)
77    {
78        if (!refresh) {
79            body.removeChildren();
80            this.sections = [];
81        }
82
83        var styleRules = [];
84
85        if (refresh) {
86            for (var i = 0; i < this.sections.length; ++i) {
87                var section = this.sections[i];
88                if (section._blank)
89                    continue;
90                if (section.computedStyle)
91                    section.styleRule.style = node.ownerDocument.defaultView.getComputedStyle(node);
92                var styleRule = { section: section, style: section.styleRule.style, computedStyle: section.computedStyle, rule: section.rule };
93                styleRules.push(styleRule);
94            }
95        } else {
96            var computedStyle = node.ownerDocument.defaultView.getComputedStyle(node);
97            styleRules.push({ computedStyle: true, selectorText: WebInspector.UIString("Computed Style"), style: computedStyle, editable: false });
98
99            var nodeName = node.nodeName.toLowerCase();
100            for (var i = 0; i < node.attributes.length; ++i) {
101                var attr = node.attributes[i];
102                if (attr.style) {
103                    var attrStyle = { style: attr.style, editable: false };
104                    attrStyle.subtitle = WebInspector.UIString("element’s “%s” attribute", attr.name);
105                    attrStyle.selectorText = nodeName + "[" + attr.name;
106                    if (attr.value.length)
107                        attrStyle.selectorText += "=" + attr.value;
108                    attrStyle.selectorText += "]";
109                    styleRules.push(attrStyle);
110                }
111            }
112
113            // Always Show element's Style Attributes
114            if (node.nodeType === Node.ELEMENT_NODE) {
115                var inlineStyle = { selectorText: WebInspector.UIString("Style Attribute"), style: node.style, isAttribute: true };
116                inlineStyle.subtitle = WebInspector.UIString("element’s “%s” attribute", "style");
117                styleRules.push(inlineStyle);
118            }
119
120            var matchedStyleRules = node.ownerDocument.defaultView.getMatchedCSSRules(node, "", !Preferences.showUserAgentStyles);
121            if (matchedStyleRules) {
122                // Add rules in reverse order to match the cascade order.
123                for (var i = (matchedStyleRules.length - 1); i >= 0; --i) {
124                    var rule = matchedStyleRules[i];
125                    styleRules.push({ style: rule.style, selectorText: rule.selectorText, parentStyleSheet: rule.parentStyleSheet, rule: rule });
126                }
127            }
128        }
129
130        function deleteDisabledProperty(style, name)
131        {
132            if (!style || !name)
133                return;
134            if (style.__disabledPropertyValues)
135                delete style.__disabledPropertyValues[name];
136            if (style.__disabledPropertyPriorities)
137                delete style.__disabledPropertyPriorities[name];
138            if (style.__disabledProperties)
139                delete style.__disabledProperties[name];
140        }
141
142        var usedProperties = {};
143        var disabledComputedProperties = {};
144        var priorityUsed = false;
145
146        // Walk the style rules and make a list of all used and overloaded properties.
147        for (var i = 0; i < styleRules.length; ++i) {
148            var styleRule = styleRules[i];
149            if (styleRule.computedStyle)
150                continue;
151            if (styleRule.section && styleRule.section.noAffect)
152                continue;
153
154            styleRule.usedProperties = {};
155
156            var style = styleRule.style;
157            for (var j = 0; j < style.length; ++j) {
158                var name = style[j];
159
160                if (!priorityUsed && style.getPropertyPriority(name).length)
161                    priorityUsed = true;
162
163                // If the property name is already used by another rule then this rule's
164                // property is overloaded, so don't add it to the rule's usedProperties.
165                if (!(name in usedProperties))
166                    styleRule.usedProperties[name] = true;
167
168                if (name === "font") {
169                    // The font property is not reported as a shorthand. Report finding the individual
170                    // properties so they are visible in computed style.
171                    // FIXME: remove this when http://bugs.webkit.org/show_bug.cgi?id=15598 is fixed.
172                    styleRule.usedProperties["font-family"] = true;
173                    styleRule.usedProperties["font-size"] = true;
174                    styleRule.usedProperties["font-style"] = true;
175                    styleRule.usedProperties["font-variant"] = true;
176                    styleRule.usedProperties["font-weight"] = true;
177                    styleRule.usedProperties["line-height"] = true;
178                }
179
180                // Delete any disabled properties, since the property does exist.
181                // This prevents it from showing twice.
182                deleteDisabledProperty(style, name);
183                deleteDisabledProperty(style, style.getPropertyShorthand(name));
184            }
185
186            // Add all the properties found in this style to the used properties list.
187            // Do this here so only future rules are affect by properties used in this rule.
188            for (var name in styleRules[i].usedProperties)
189                usedProperties[name] = true;
190
191            // Remember all disabled properties so they show up in computed style.
192            if (style.__disabledProperties)
193                for (var name in style.__disabledProperties)
194                    disabledComputedProperties[name] = true;
195        }
196
197        if (priorityUsed) {
198            // Walk the properties again and account for !important.
199            var foundPriorityProperties = [];
200
201            // Walk in reverse to match the order !important overrides.
202            for (var i = (styleRules.length - 1); i >= 0; --i) {
203                if (styleRules[i].computedStyle)
204                    continue;
205
206                var style = styleRules[i].style;
207                var uniqueProperties = style.uniqueStyleProperties;
208                for (var j = 0; j < uniqueProperties.length; ++j) {
209                    var name = uniqueProperties[j];
210                    if (style.getPropertyPriority(name).length) {
211                        if (!(name in foundPriorityProperties))
212                            styleRules[i].usedProperties[name] = true;
213                        else
214                            delete styleRules[i].usedProperties[name];
215                        foundPriorityProperties[name] = true;
216                    } else if (name in foundPriorityProperties)
217                        delete styleRules[i].usedProperties[name];
218                }
219            }
220        }
221
222        if (refresh) {
223            // Walk the style rules and update the sections with new overloaded and used properties.
224            for (var i = 0; i < styleRules.length; ++i) {
225                var styleRule = styleRules[i];
226                var section = styleRule.section;
227                if (styleRule.computedStyle)
228                    section.disabledComputedProperties = disabledComputedProperties;
229                section._usedProperties = (styleRule.usedProperties || usedProperties);
230                section.update((section === editedSection) || styleRule.computedStyle);
231            }
232        } else {
233            // Make a property section for each style rule.
234            for (var i = 0; i < styleRules.length; ++i) {
235                var styleRule = styleRules[i];
236                var subtitle = styleRule.subtitle;
237                delete styleRule.subtitle;
238
239                var computedStyle = styleRule.computedStyle;
240                delete styleRule.computedStyle;
241
242                var ruleUsedProperties = styleRule.usedProperties;
243                delete styleRule.usedProperties;
244
245                var editable = styleRule.editable;
246                delete styleRule.editable;
247
248                var isAttribute = styleRule.isAttribute;
249                delete styleRule.isAttribute;
250
251                // Default editable to true if it was omitted.
252                if (typeof editable === "undefined")
253                    editable = true;
254
255                var section = new WebInspector.StylePropertiesSection(styleRule, subtitle, computedStyle, (ruleUsedProperties || usedProperties), editable);
256                if (computedStyle)
257                    section.disabledComputedProperties = disabledComputedProperties;
258                section.pane = this;
259
260                if (Preferences.styleRulesExpandedState && section.identifier in Preferences.styleRulesExpandedState)
261                    section.expanded = Preferences.styleRulesExpandedState[section.identifier];
262                else if (computedStyle)
263                    section.collapse(true);
264                else if (isAttribute && styleRule.style.length === 0)
265                    section.collapse(true);
266                else
267                    section.expand(true);
268
269                body.appendChild(section.element);
270                this.sections.push(section);
271            }
272
273            this.addBlankSection();
274        }
275    },
276
277    addBlankSection: function()
278    {
279        var blankSection = new WebInspector.BlankStylePropertiesSection();
280        blankSection.pane = this;
281
282        this.bodyElement.insertBefore(blankSection.element, this.bodyElement.firstChild.nextSibling.nextSibling); // 0 is computed, 1 is element.style
283        var computed = this.sections.shift();
284        var elementStyle = this.sections.shift();
285        this.sections.unshift(blankSection);
286        this.sections.unshift(elementStyle);
287        this.sections.unshift(computed);
288    },
289
290    appropriateSelectorForNode: function()
291    {
292        var node = this.node;
293        if (!node)
294            return;
295
296        if (node.id)
297            return "#" + node.id;
298
299        if (node.className)
300            return "." + node.className.replace(/\s+/, ".");
301
302        var nodeName = node.nodeName.toLowerCase();
303        if (nodeName === "input" && node.type)
304            return nodeName + "[type=\"" + node.type + "\"]";
305
306        return nodeName;
307    }
308}
309
310WebInspector.StylesSidebarPane.prototype.__proto__ = WebInspector.SidebarPane.prototype;
311
312WebInspector.StylePropertiesSection = function(styleRule, subtitle, computedStyle, usedProperties, editable)
313{
314    WebInspector.PropertiesSection.call(this, styleRule.selectorText);
315    this.titleElement.addEventListener("click", function(e) { e.stopPropagation(); }, false);
316    this.titleElement.addEventListener("dblclick", this._dblclickSelector.bind(this), false);
317    this.element.addEventListener("dblclick", this._dblclickEmptySpace.bind(this), false);
318
319    this.styleRule = styleRule;
320    this.rule = this.styleRule.rule;
321    this.computedStyle = computedStyle;
322    this.editable = (editable && !computedStyle);
323
324    // Prevent editing the user agent and user rules.
325    var isUserAgent = this.styleRule.isUserAgent;
326    var isUser = this.styleRule.isUser;
327
328    if (isUserAgent || isUser)
329        this.editable = false;
330
331    this._usedProperties = usedProperties;
332
333    if (computedStyle) {
334        this.element.addStyleClass("computed-style");
335
336        if (Preferences.showInheritedComputedStyleProperties)
337            this.element.addStyleClass("show-inherited");
338
339        var showInheritedLabel = document.createElement("label");
340        var showInheritedInput = document.createElement("input");
341        showInheritedInput.type = "checkbox";
342        showInheritedInput.checked = Preferences.showInheritedComputedStyleProperties;
343
344        var computedStyleSection = this;
345        var showInheritedToggleFunction = function(event) {
346            Preferences.showInheritedComputedStyleProperties = showInheritedInput.checked;
347            if (Preferences.showInheritedComputedStyleProperties)
348                computedStyleSection.element.addStyleClass("show-inherited");
349            else
350                computedStyleSection.element.removeStyleClass("show-inherited");
351            event.stopPropagation();
352        };
353
354        showInheritedLabel.addEventListener("click", showInheritedToggleFunction, false);
355
356        showInheritedLabel.appendChild(showInheritedInput);
357        showInheritedLabel.appendChild(document.createTextNode(WebInspector.UIString("Show inherited")));
358        this.subtitleElement.appendChild(showInheritedLabel);
359    } else {
360        if (!subtitle) {
361            if (this.styleRule.parentStyleSheet && this.styleRule.parentStyleSheet.href) {
362                var url = this.styleRule.parentStyleSheet.href;
363                subtitle = WebInspector.linkifyURL(url, WebInspector.displayNameForURL(url));
364                this.subtitleElement.addStyleClass("file");
365            } else if (isUserAgent)
366                subtitle = WebInspector.UIString("user agent stylesheet");
367            else if (isUser)
368                subtitle = WebInspector.UIString("user stylesheet");
369            else if (this.styleRule.parentStyleSheet === WebInspector.panels.elements.stylesheet)
370                subtitle = WebInspector.UIString("via inspector");
371            else
372                subtitle = WebInspector.UIString("inline stylesheet");
373        }
374
375        this.subtitle = subtitle;
376    }
377
378    this.identifier = styleRule.selectorText;
379    if (this.subtitle)
380        this.identifier += ":" + this.subtitleElement.textContent;
381}
382
383WebInspector.StylePropertiesSection.prototype = {
384    get usedProperties()
385    {
386        return this._usedProperties || {};
387    },
388
389    set usedProperties(x)
390    {
391        this._usedProperties = x;
392        this.update();
393    },
394
395    expand: function(dontRememberState)
396    {
397        if (this._blank)
398            return;
399
400        WebInspector.PropertiesSection.prototype.expand.call(this);
401        if (dontRememberState)
402            return;
403
404        if (!Preferences.styleRulesExpandedState)
405            Preferences.styleRulesExpandedState = {};
406        Preferences.styleRulesExpandedState[this.identifier] = true;
407    },
408
409    collapse: function(dontRememberState)
410    {
411        WebInspector.PropertiesSection.prototype.collapse.call(this);
412        if (dontRememberState)
413            return;
414
415        if (!Preferences.styleRulesExpandedState)
416            Preferences.styleRulesExpandedState = {};
417        Preferences.styleRulesExpandedState[this.identifier] = false;
418    },
419
420    isPropertyInherited: function(property)
421    {
422        if (!this.computedStyle || !this._usedProperties || this.noAffect)
423            return false;
424        // These properties should always show for Computed Style.
425        var alwaysShowComputedProperties = { "display": true, "height": true, "width": true };
426        return !(property in this.usedProperties) && !(property in alwaysShowComputedProperties) && !(property in this.disabledComputedProperties);
427    },
428
429    isPropertyOverloaded: function(property, shorthand)
430    {
431        if (this.computedStyle || !this._usedProperties || this.noAffect)
432            return false;
433
434        var used = (property in this.usedProperties);
435        if (used || !shorthand)
436            return !used;
437
438        // Find out if any of the individual longhand properties of the shorthand
439        // are used, if none are then the shorthand is overloaded too.
440        var longhandProperties = this.styleRule.style.getLonghandProperties(property);
441        for (var j = 0; j < longhandProperties.length; ++j) {
442            var individualProperty = longhandProperties[j];
443            if (individualProperty in this.usedProperties)
444                return false;
445        }
446
447        return true;
448    },
449
450    isInspectorStylesheet: function()
451    {
452        return (this.styleRule.parentStyleSheet === WebInspector.panels.elements.stylesheet);
453    },
454
455    update: function(full)
456    {
457        if (full || this.computedStyle) {
458            this.propertiesTreeOutline.removeChildren();
459            this.populated = false;
460        } else {
461            var child = this.propertiesTreeOutline.children[0];
462            while (child) {
463                child.overloaded = this.isPropertyOverloaded(child.name, child.shorthand);
464                child = child.traverseNextTreeElement(false, null, true);
465            }
466        }
467
468        if (this._afterUpdate) {
469            this._afterUpdate(this);
470            delete this._afterUpdate;
471        }
472    },
473
474    onpopulate: function()
475    {
476        var style = this.styleRule.style;
477
478        var foundShorthands = {};
479        var uniqueProperties = style.uniqueStyleProperties;
480        var disabledProperties = style.__disabledPropertyValues || {};
481
482        for (var name in disabledProperties)
483            uniqueProperties.push(name);
484
485        uniqueProperties.sort();
486
487        for (var i = 0; i < uniqueProperties.length; ++i) {
488            var name = uniqueProperties[i];
489            var disabled = name in disabledProperties;
490            if (!disabled && this.disabledComputedProperties && !(name in this.usedProperties) && name in this.disabledComputedProperties)
491                disabled = true;
492
493            var shorthand = !disabled ? style.getPropertyShorthand(name) : null;
494
495            if (shorthand && shorthand in foundShorthands)
496                continue;
497
498            if (shorthand) {
499                foundShorthands[shorthand] = true;
500                name = shorthand;
501            }
502
503            var isShorthand = (shorthand ? true : false);
504            var inherited = this.isPropertyInherited(name);
505            var overloaded = this.isPropertyOverloaded(name, isShorthand);
506
507            var item = new WebInspector.StylePropertyTreeElement(this.styleRule, style, name, isShorthand, inherited, overloaded, disabled);
508            this.propertiesTreeOutline.appendChild(item);
509        }
510    },
511
512    findTreeElementWithName: function(name)
513    {
514        var treeElement = this.propertiesTreeOutline.children[0];
515        while (treeElement) {
516            if (treeElement.name === name)
517                return treeElement;
518            treeElement = treeElement.traverseNextTreeElement(true, null, true);
519        }
520        return null;
521    },
522
523    addNewBlankProperty: function()
524    {
525        var item = new WebInspector.StylePropertyTreeElement(this.styleRule, this.styleRule.style, "", false, false, false, false);
526        this.propertiesTreeOutline.appendChild(item);
527        item.listItemElement.textContent = "";
528        item._newProperty = true;
529        return item;
530    },
531
532    _dblclickEmptySpace: function(event)
533    {
534        this.expand();
535        this.addNewBlankProperty().startEditing();
536    },
537
538    _dblclickSelector: function(event)
539    {
540        if (!this.editable)
541            return;
542
543        if (!this.rule && this.propertiesTreeOutline.children.length === 0) {
544            this.expand();
545            this.addNewBlankProperty().startEditing();
546            return;
547        }
548
549        if (!this.rule)
550            return;
551
552        this.startEditingSelector();
553        event.stopPropagation();
554    },
555
556    startEditingSelector: function()
557    {
558        var element = this.titleElement;
559        if (WebInspector.isBeingEdited(element))
560            return;
561
562        var context = this.styleRule.selectorText;
563        WebInspector.startEditing(this.titleElement, this.editingSelectorCommitted.bind(this), this.editingSelectorCancelled.bind(this), context);
564        window.getSelection().setBaseAndExtent(element, 0, element, 1);
565    },
566
567    editingSelectorCommitted: function(element, newContent, oldContent, context, moveDirection)
568    {
569        function moveToNextIfNeeded() {
570            if (!moveDirection || moveDirection !== "forward")
571                return;
572
573            this.expand();
574            if (this.propertiesTreeOutline.children.length === 0)
575                this.addNewBlankProperty().startEditing();
576            else {
577                var item = this.propertiesTreeOutline.children[0]
578                item.startEditing(item.valueElement);
579            }
580        }
581
582        if (newContent === oldContent)
583            return moveToNextIfNeeded.call(this);
584
585        var self = this;
586        var callback = function(result) {
587            if (!result) {
588                // Invalid Syntax for a Selector
589                self.editingSelectorCancelled(element, context);
590                moveToNextIfNeeded.call(self);
591                return;
592            }
593
594            var newRulePayload = result[0];
595            var doesAffectSelectedNode = result[1];
596            if (!doesAffectSelectedNode) {
597                self.noAffect = true;
598                self.element.addStyleClass("no-affect");
599            } else {
600                delete self.noAffect;
601                self.element.removeStyleClass("no-affect");
602            }
603
604            var newRule = WebInspector.CSSStyleDeclaration.parseRule(newRulePayload);
605            self.rule = newRule;
606            self.styleRule = { section: self, style: newRule.style, selectorText: newRule.selectorText, parentStyleSheet: newRule.parentStyleSheet, rule: newRule };
607            var oldIdentifier = this.identifier;
608            self.identifier = newRule.selectorText + ":" + self.subtitleElement.textContent;
609            self.pane.update(null, true);
610            WebInspector.panels.elements.renameSelector(oldIdentifier, this.identifier, oldContent, newContent);
611            moveToNextIfNeeded.call(self);
612        };
613
614        InspectorController.applyStyleRuleText(this.rule._id, newContent, this.pane.node, callback);
615    },
616
617    editingSelectorCancelled: function(element, context)
618    {
619        element.textContent = context;
620    },
621
622    _doesSelectorAffectSelectedNode: function(selector)
623    {
624        var selectedNode = this.pane.node;
625        var nodes = selectedNode.ownerDocument.querySelectorAll(selector);
626        for (var i = 0; i < nodes.length; ++i) {
627            if (nodes[i] === selectedNode)
628                return true;
629        }
630
631        return false;
632    }
633}
634
635WebInspector.StylePropertiesSection.prototype.__proto__ = WebInspector.PropertiesSection.prototype;
636
637WebInspector.BlankStylePropertiesSection = function()
638{
639    WebInspector.PropertiesSection.call(this, WebInspector.UIString("Double-Click to Add"), null);
640
641    this._blank = true;
642    this._dblclickListener = this._dblclick.bind(this);
643    this.element.addStyleClass("blank-section");
644    this.titleElement.addStyleClass("blank-title");
645    this.titleElement.addEventListener("click", function(e) { e.stopPropagation(); }, false);
646    this.titleElement.addEventListener("dblclick", this._dblclickListener, false);
647}
648
649WebInspector.BlankStylePropertiesSection.prototype = {
650    _dblclick: function(event)
651    {
652        this.startEditing();
653    },
654
655    startEditing: function()
656    {
657        var element = this.titleElement;
658        if (WebInspector.isBeingEdited(element))
659            return;
660
661        this.titleElement.textContent = this.pane.appropriateSelectorForNode();
662        this.titleElement.removeStyleClass("blank-title");
663        WebInspector.startEditing(this.titleElement, this.editingCommitted.bind(this), this.editingCancelled.bind(this), "");
664        window.getSelection().setBaseAndExtent(element, 0, element, 1);
665    },
666
667    editingCancelled: function()
668    {
669        this.titleElement.textContent = WebInspector.UIString("Double-Click to Add");
670        this.titleElement.addStyleClass("blank-title");
671    },
672
673    editingCommitted: function(element, newContent, oldContent, context)
674    {
675        var self = this;
676        var callback = function(styleRule) {
677            if (!styleRule) {
678                // Invalid Syntax for a Selector
679                self.editingCancelled();
680                return;
681            }
682            self.makeNormal(WebInspector.CSSStyleDeclaration.parseRule(styleRule));
683
684            if (!self._doesSelectorAffectSelectedNode(newContent)) {
685                self.noAffect = true;
686                self.element.addStyleClass("no-affect");
687            }
688
689            self.subtitleElement.textContent = WebInspector.UIString("via inspector");
690            self.expand();
691
692            self.pane.addBlankSection();
693            self.addNewBlankProperty().startEditing();
694        };
695        InspectorController.addStyleSelector(newContent, callback);
696    },
697
698    makeNormal: function(styleRule)
699    {
700        this.titleElement.removeEventListener("dblclick", this._dblclickListener, false);
701        this.titleElement.addEventListener("dblclick", this._dblclickSelector.bind(this), false);
702        this.element.addEventListener("dblclick", this._dblclickEmptySpace.bind(this), false);
703        this.element.removeStyleClass("blank-section");
704        delete this._blank;
705        delete this._dblclick;
706        delete this.startEditing;
707        delete this.editingCancelled;
708        delete this.editingCommitted;
709        delete this._dblclickListener;
710        delete this.makeNormal;
711        this.styleRule = styleRule;
712        this.rule = styleRule.rule;
713        this.computedStyle = false;
714        this.editable = true;
715        this.identifier = styleRule.selectorText + ":inspector";
716        // leftovers are: this.noAffect if applicable
717    }
718}
719
720WebInspector.BlankStylePropertiesSection.prototype.__proto__ = WebInspector.StylePropertiesSection.prototype;
721
722WebInspector.StylePropertyTreeElement = function(styleRule, style, name, shorthand, inherited, overloaded, disabled)
723{
724    this._styleRule = styleRule;
725    this.style = style;
726    this.name = name;
727    this.shorthand = shorthand;
728    this._inherited = inherited;
729    this._overloaded = overloaded;
730    this._disabled = disabled;
731
732    // Pass an empty title, the title gets made later in onattach.
733    TreeElement.call(this, "", null, shorthand);
734}
735
736WebInspector.StylePropertyTreeElement.prototype = {
737    get inherited()
738    {
739        return this._inherited;
740    },
741
742    set inherited(x)
743    {
744        if (x === this._inherited)
745            return;
746        this._inherited = x;
747        this.updateState();
748    },
749
750    get overloaded()
751    {
752        return this._overloaded;
753    },
754
755    set overloaded(x)
756    {
757        if (x === this._overloaded)
758            return;
759        this._overloaded = x;
760        this.updateState();
761    },
762
763    get disabled()
764    {
765        return this._disabled;
766    },
767
768    set disabled(x)
769    {
770        if (x === this._disabled)
771            return;
772        this._disabled = x;
773        this.updateState();
774    },
775
776    get priority()
777    {
778        if (this.disabled && this.style.__disabledPropertyPriorities && this.name in this.style.__disabledPropertyPriorities)
779            return this.style.__disabledPropertyPriorities[this.name];
780        return (this.shorthand ? this.style.getShorthandPriority(this.name) : this.style.getPropertyPriority(this.name));
781    },
782
783    get value()
784    {
785        if (this.disabled && this.style.__disabledPropertyValues && this.name in this.style.__disabledPropertyValues)
786            return this.style.__disabledPropertyValues[this.name];
787        return (this.shorthand ? this.style.getShorthandValue(this.name) : this.style.getPropertyValue(this.name));
788    },
789
790    onattach: function()
791    {
792        this.updateTitle();
793    },
794
795    updateTitle: function()
796    {
797        // "Nicknames" for some common values that are easier to read.
798        var valueNicknames = {
799            "rgb(0, 0, 0)": "black",
800            "#000": "black",
801            "#000000": "black",
802            "rgb(255, 255, 255)": "white",
803            "#fff": "white",
804            "#ffffff": "white",
805            "#FFF": "white",
806            "#FFFFFF": "white",
807            "rgba(0, 0, 0, 0)": "transparent",
808            "rgb(255, 0, 0)": "red",
809            "rgb(0, 255, 0)": "lime",
810            "rgb(0, 0, 255)": "blue",
811            "rgb(255, 255, 0)": "yellow",
812            "rgb(255, 0, 255)": "magenta",
813            "rgb(0, 255, 255)": "cyan"
814        };
815
816        var priority = this.priority;
817        var value = this.value;
818        var htmlValue = value;
819
820        if (priority && !priority.length)
821            delete priority;
822        if (priority)
823            priority = "!" + priority;
824
825        if (value) {
826            var urls = value.match(/url\([^)]+\)/);
827            if (urls) {
828                for (var i = 0; i < urls.length; ++i) {
829                    var url = urls[i].substring(4, urls[i].length - 1);
830                    htmlValue = htmlValue.replace(urls[i], "url(" + WebInspector.linkifyURL(url) + ")");
831                }
832            } else {
833                if (value in valueNicknames)
834                    htmlValue = valueNicknames[value];
835                htmlValue = htmlValue.escapeHTML();
836            }
837        } else
838            htmlValue = value = "";
839
840        this.updateState();
841
842        var enabledCheckboxElement = document.createElement("input");
843        enabledCheckboxElement.className = "enabled-button";
844        enabledCheckboxElement.type = "checkbox";
845        enabledCheckboxElement.checked = !this.disabled;
846        enabledCheckboxElement.addEventListener("change", this.toggleEnabled.bind(this), false);
847
848        var nameElement = document.createElement("span");
849        nameElement.className = "name";
850        nameElement.textContent = this.name;
851        this.nameElement = nameElement;
852
853        var valueElement = document.createElement("span");
854        valueElement.className = "value";
855        valueElement.innerHTML = htmlValue;
856        this.valueElement = valueElement;
857
858        if (priority) {
859            var priorityElement = document.createElement("span");
860            priorityElement.className = "priority";
861            priorityElement.textContent = priority;
862        }
863
864        this.listItemElement.removeChildren();
865
866        // Append the checkbox for root elements of an editable section.
867        if (this.treeOutline.section && this.treeOutline.section.editable && this.parent.root)
868            this.listItemElement.appendChild(enabledCheckboxElement);
869        this.listItemElement.appendChild(nameElement);
870        this.listItemElement.appendChild(document.createTextNode(": "));
871        this.listItemElement.appendChild(valueElement);
872
873        if (priorityElement) {
874            this.listItemElement.appendChild(document.createTextNode(" "));
875            this.listItemElement.appendChild(priorityElement);
876        }
877
878        this.listItemElement.appendChild(document.createTextNode(";"));
879
880        if (value) {
881            // FIXME: this only covers W3C and CSS 16 valid color names
882            var colors = value.match(/((rgb|hsl)a?\([^)]+\))|(#[0-9a-fA-F]{6})|(#[0-9a-fA-F]{3})|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow|transparent/g);
883            var swatch;
884            if (colors) {
885                var colorsLength = colors.length;
886                for (var i = 0; i < colorsLength; ++i) {
887                    var swatchElement = document.createElement("span");
888                    swatchElement.className = "swatch";
889                    swatchElement.style.setProperty("background-color", colors[i]);
890                    this.listItemElement.appendChild(swatchElement);
891                    swatch = swatchElement;
892                }
893            }
894
895            // Rotate through Color Representations by Clicking on the Swatch
896            // Simple: rgb -> hsl -> nickname? -> shorthex? -> hex -> ...
897            // Advanced: rgba -> hsla -> nickname? -> ...
898            if (colors && colors.length === 1) {
899                var color = new WebInspector.Color(htmlValue);
900                swatch.addEventListener("click", changeColorDisplay, false);
901                swatch.addEventListener("dblclick", function(event) {
902                    event.stopPropagation();
903                }, false);
904
905                var mode = color.mode;
906                var valueElement = this.valueElement;
907                function changeColorDisplay(event) {
908
909                    function changeTo(newMode, content) {
910                        mode = newMode;
911                        valueElement.textContent = content;
912                    }
913
914                    switch (mode) {
915                        case "rgb":
916                            changeTo("hsl", color.toHsl());
917                            break;
918
919                        case "shorthex":
920                            changeTo("hex", color.toHex());
921                            break;
922
923                        case "hex":
924                            changeTo("rgb", color.toRgb());
925                            break;
926
927                        case "nickname":
928                            if (color.simple) {
929                                if (color.hasShortHex())
930                                    changeTo("shorthex", color.toShortHex());
931                                else
932                                    changeTo("hex", color.toHex());
933                            } else
934                                changeTo("rgba", color.toRgba());
935                            break;
936
937                        case "hsl":
938                            if (color.nickname)
939                                changeTo("nickname", color.toNickname());
940                            else if (color.hasShortHex())
941                                changeTo("shorthex", color.toShortHex());
942                            else
943                                changeTo("hex", color.toHex());
944                            break;
945
946                        case "rgba":
947                            changeTo("hsla", color.toHsla());
948                            break;
949
950                        case "hsla":
951                            if (color.nickname)
952                                changeTo("nickname", color.toNickname());
953                            else
954                                changeTo("rgba", color.toRgba());
955                            break;
956                    }
957                }
958            }
959        }
960
961        this.tooltip = this.name + ": " + (valueNicknames[value] || value) + (priority ? " " + priority : "");
962    },
963
964    updateAll: function(updateAllRules)
965    {
966        if (updateAllRules && this.treeOutline.section && this.treeOutline.section.pane)
967            this.treeOutline.section.pane.update(null, this.treeOutline.section);
968        else if (this.treeOutline.section)
969            this.treeOutline.section.update(true);
970        else
971            this.updateTitle(); // FIXME: this will not show new properties. But we don't hit his case yet.
972    },
973
974    toggleEnabled: function(event)
975    {
976        var disabled = !event.target.checked;
977
978        var self = this;
979        var callback = function(newPayload) {
980            if (!newPayload)
981                return;
982
983            self.style = WebInspector.CSSStyleDeclaration.parseStyle(newPayload);
984            self._styleRule.style = self.style;
985
986            // Set the disabled property here, since the code above replies on it not changing
987            // until after the value and priority are retrieved.
988            self.disabled = disabled;
989
990            if (self.treeOutline.section && self.treeOutline.section.pane)
991                self.treeOutline.section.pane.dispatchEventToListeners("style property toggled");
992
993            self.updateAll(true);
994        };
995        InspectorController.toggleStyleEnabled(this.style._id, this.name, disabled, callback);
996    },
997
998    updateState: function()
999    {
1000        if (!this.listItemElement)
1001            return;
1002
1003        if (this.style.isPropertyImplicit(this.name) || this.value === "initial")
1004            this.listItemElement.addStyleClass("implicit");
1005        else
1006            this.listItemElement.removeStyleClass("implicit");
1007
1008        if (this.inherited)
1009            this.listItemElement.addStyleClass("inherited");
1010        else
1011            this.listItemElement.removeStyleClass("inherited");
1012
1013        if (this.overloaded)
1014            this.listItemElement.addStyleClass("overloaded");
1015        else
1016            this.listItemElement.removeStyleClass("overloaded");
1017
1018        if (this.disabled)
1019            this.listItemElement.addStyleClass("disabled");
1020        else
1021            this.listItemElement.removeStyleClass("disabled");
1022    },
1023
1024    onpopulate: function()
1025    {
1026        // Only populate once and if this property is a shorthand.
1027        if (this.children.length || !this.shorthand)
1028            return;
1029
1030        var longhandProperties = this.style.getLonghandProperties(this.name);
1031        for (var i = 0; i < longhandProperties.length; ++i) {
1032            var name = longhandProperties[i];
1033
1034            if (this.treeOutline.section) {
1035                var inherited = this.treeOutline.section.isPropertyInherited(name);
1036                var overloaded = this.treeOutline.section.isPropertyOverloaded(name);
1037            }
1038
1039            var item = new WebInspector.StylePropertyTreeElement(this._styleRule, this.style, name, false, inherited, overloaded);
1040            this.appendChild(item);
1041        }
1042    },
1043
1044    ondblclick: function(element, event)
1045    {
1046        this.startEditing(event.target);
1047        event.stopPropagation();
1048    },
1049
1050    startEditing: function(selectElement)
1051    {
1052        // FIXME: we don't allow editing of longhand properties under a shorthand right now.
1053        if (this.parent.shorthand)
1054            return;
1055
1056        if (WebInspector.isBeingEdited(this.listItemElement) || (this.treeOutline.section && !this.treeOutline.section.editable))
1057            return;
1058
1059        var context = { expanded: this.expanded, hasChildren: this.hasChildren };
1060
1061        // Lie about our children to prevent expanding on double click and to collapse shorthands.
1062        this.hasChildren = false;
1063
1064        if (!selectElement)
1065            selectElement = this.listItemElement;
1066
1067        this.listItemElement.handleKeyEvent = this.editingKeyDown.bind(this);
1068
1069        WebInspector.startEditing(this.listItemElement, this.editingCommitted.bind(this), this.editingCancelled.bind(this), context);
1070        window.getSelection().setBaseAndExtent(selectElement, 0, selectElement, 1);
1071    },
1072
1073    editingKeyDown: function(event)
1074    {
1075        var arrowKeyPressed = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down");
1076        var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown");
1077        if (!arrowKeyPressed && !pageKeyPressed)
1078            return;
1079
1080        var selection = window.getSelection();
1081        if (!selection.rangeCount)
1082            return;
1083
1084        var selectionRange = selection.getRangeAt(0);
1085        if (selectionRange.commonAncestorContainer !== this.listItemElement && !selectionRange.commonAncestorContainer.isDescendant(this.listItemElement))
1086            return;
1087
1088        const styleValueDelimeters = " \t\n\"':;,/()";
1089        var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, styleValueDelimeters, this.listItemElement);
1090        var wordString = wordRange.toString();
1091        var replacementString = wordString;
1092
1093        var matches = /(.*?)(-?\d+(?:\.\d+)?)(.*)/.exec(wordString);
1094        if (matches && matches.length) {
1095            var prefix = matches[1];
1096            var number = parseFloat(matches[2]);
1097            var suffix = matches[3];
1098
1099            // If the number is near zero or the number is one and the direction will take it near zero.
1100            var numberNearZero = (number < 1 && number > -1);
1101            if (number === 1 && event.keyIdentifier === "Down")
1102                numberNearZero = true;
1103            else if (number === -1 && event.keyIdentifier === "Up")
1104                numberNearZero = true;
1105
1106            if (numberNearZero && event.altKey && arrowKeyPressed) {
1107                if (event.keyIdentifier === "Down")
1108                    number = Math.ceil(number - 1);
1109                else
1110                    number = Math.floor(number + 1);
1111            } else {
1112                // Jump by 10 when shift is down or jump by 0.1 when near zero or Alt/Option is down.
1113                // Also jump by 10 for page up and down, or by 100 if shift is held with a page key.
1114                var changeAmount = 1;
1115                if (event.shiftKey && pageKeyPressed)
1116                    changeAmount = 100;
1117                else if (event.shiftKey || pageKeyPressed)
1118                    changeAmount = 10;
1119                else if (event.altKey || numberNearZero)
1120                    changeAmount = 0.1;
1121
1122                if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown")
1123                    changeAmount *= -1;
1124
1125                // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
1126                // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
1127                number = Number((number + changeAmount).toFixed(6));
1128            }
1129
1130            replacementString = prefix + number + suffix;
1131        } else {
1132            // FIXME: this should cycle through known keywords for the current property name.
1133            return;
1134        }
1135
1136        var replacementTextNode = document.createTextNode(replacementString);
1137
1138        wordRange.deleteContents();
1139        wordRange.insertNode(replacementTextNode);
1140
1141        var finalSelectionRange = document.createRange();
1142        finalSelectionRange.setStart(replacementTextNode, 0);
1143        finalSelectionRange.setEnd(replacementTextNode, replacementString.length);
1144
1145        selection.removeAllRanges();
1146        selection.addRange(finalSelectionRange);
1147
1148        event.preventDefault();
1149        event.handled = true;
1150
1151        if (!this.originalCSSText) {
1152            // Remember the rule's original CSS text, so it can be restored
1153            // if the editing is canceled and before each apply.
1154            this.originalCSSText = this.style.styleTextWithShorthands();
1155        } else {
1156            // Restore the original CSS text before applying user changes. This is needed to prevent
1157            // new properties from sticking around if the user adds one, then removes it.
1158            InspectorController.setStyleText(this.style, this.originalCSSText);
1159        }
1160
1161        this.applyStyleText(this.listItemElement.textContent);
1162    },
1163
1164    editingEnded: function(context)
1165    {
1166        this.hasChildren = context.hasChildren;
1167        if (context.expanded)
1168            this.expand();
1169        delete this.listItemElement.handleKeyEvent;
1170        delete this.originalCSSText;
1171    },
1172
1173    editingCancelled: function(element, context)
1174    {
1175        if (this._newProperty)
1176            this.treeOutline.removeChild(this);
1177        else if (this.originalCSSText) {
1178            InspectorController.setStyleText(this.style, this.originalCSSText);
1179
1180            if (this.treeOutline.section && this.treeOutline.section.pane)
1181                this.treeOutline.section.pane.dispatchEventToListeners("style edited");
1182
1183            this.updateAll();
1184        } else
1185            this.updateTitle();
1186
1187        this.editingEnded(context);
1188    },
1189
1190    editingCommitted: function(element, userInput, previousContent, context, moveDirection)
1191    {
1192        this.editingEnded(context);
1193
1194        // Determine where to move to before making changes
1195        var newProperty, moveToPropertyName, moveToSelector;
1196        var moveTo = (moveDirection === "forward" ? this.nextSibling : this.previousSibling);
1197        if (moveTo)
1198            moveToPropertyName = moveTo.name;
1199        else if (moveDirection === "forward")
1200            newProperty = true;
1201        else if (moveDirection === "backward" && this.treeOutline.section.rule)
1202            moveToSelector = true;
1203
1204        // Make the Changes and trigger the moveToNextCallback after updating
1205        var blankInput = /^\s*$/.test(userInput);
1206        if (userInput !== previousContent || (this._newProperty && blankInput)) { // only if something changed, or adding a new style and it was blank
1207            this.treeOutline.section._afterUpdate = moveToNextCallback.bind(this, this._newProperty, !blankInput);
1208            this.applyStyleText(userInput, true);
1209        } else
1210            moveToNextCallback(this._newProperty, false, this.treeOutline.section, false);
1211
1212        // The Callback to start editing the next property
1213        function moveToNextCallback(alreadyNew, valueChanged, section) {
1214            if (!moveDirection)
1215                return;
1216
1217            // User just tabbed through without changes
1218            if (moveTo && moveTo.parent) {
1219                moveTo.startEditing(moveTo.valueElement);
1220                return;
1221            }
1222
1223            // User has made a change then tabbed, wiping all the original treeElements,
1224            // recalculate the new treeElement for the same property we were going to edit next
1225            if (moveTo && !moveTo.parent) {
1226                var treeElement = section.findTreeElementWithName(moveToPropertyName);
1227                if (treeElement)
1228                    treeElement.startEditing(treeElement.valueElement);
1229                return;
1230            }
1231
1232            // Create a new attribute in this section
1233            if (newProperty) {
1234                if (alreadyNew && !valueChanged)
1235                    return;
1236
1237                var item = section.addNewBlankProperty();
1238                item.startEditing();
1239                return;
1240            }
1241
1242            if (moveToSelector)
1243                section.startEditingSelector();
1244        }
1245    },
1246
1247    applyStyleText: function(styleText, updateInterface)
1248    {
1249        var section = this.treeOutline.section;
1250        var elementsPanel = WebInspector.panels.elements;
1251        var styleTextLength = styleText.trimWhitespace().length;
1252        if (!styleTextLength) {
1253            if (updateInterface) {
1254                // The user deleted everything, so remove the tree element and update.
1255                if (!this._newProperty)
1256                    delete section._afterUpdate;
1257                if (section && section.pane)
1258                    section.pane.update();
1259                this.parent.removeChild(this);
1260                elementsPanel.removeStyleChange(section.identifier, this.style, this.name);
1261            }
1262            return;
1263        }
1264
1265        var self = this;
1266        var callback = function(result) {
1267            if (!result) {
1268                // The user typed something, but it didn't parse. Just abort and restore
1269                // the original title for this property.  If this was a new attribute and
1270                // we couldn't parse, then just remove it.
1271                if (self._newProperty) {
1272                    self.parent.removeChild(self);
1273                    return;
1274                }
1275                if (updateInterface)
1276                    self.updateTitle();
1277                return;
1278            }
1279
1280            var newPayload = result[0];
1281            var changedProperties = result[1];
1282            elementsPanel.removeStyleChange(section.identifier, self.style, self.name);
1283
1284            self.style = WebInspector.CSSStyleDeclaration.parseStyle(newPayload);
1285            for (var i = 0; i < changedProperties.length; ++i)
1286                elementsPanel.addStyleChange(section.identifier, self.style, changedProperties[i]);
1287            self._styleRule.style = self.style;
1288            if (section && section.pane)
1289                section.pane.dispatchEventToListeners("style edited");
1290
1291            if (updateInterface)
1292                self.updateAll(true);
1293
1294            if (!self.rule)
1295                WebInspector.panels.elements.treeOutline.update();
1296        };
1297        InspectorController.applyStyleText(this.style._id, styleText.trimWhitespace(), this.name, callback);
1298    }
1299}
1300
1301WebInspector.StylePropertyTreeElement.prototype.__proto__ = TreeElement.prototype;
1302