Web Inspector: Support smart-pasting in the Rules sidebar panel
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CSSStyleDeclarationSection.js
1 /*
2  * Copyright (C) 2013 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.CSSStyleDeclarationSection = function(delegate, style)
27 {
28     // FIXME: Convert this to a WebInspector.Object subclass, and call super().
29     // WebInspector.Object.call(this);
30
31     this._delegate = delegate || null;
32
33     console.assert(style);
34     this._style = style || null;
35     this._selectorElements = [];
36     this._ruleDisabled = false;
37
38     this._element = document.createElement("div");
39     this._element.className = "style-declaration-section";
40
41     this._headerElement = document.createElement("div");
42     this._headerElement.className = "header";
43
44     this._iconElement = document.createElement("img");
45     this._iconElement.className = "icon";
46     this._headerElement.appendChild(this._iconElement);
47
48     this._selectorElement = document.createElement("span");
49     this._selectorElement.className = "selector";
50     this._selectorElement.setAttribute("spellcheck", "false");
51     this._selectorElement.addEventListener("mouseover", this._handleMouseOver.bind(this));
52     this._selectorElement.addEventListener("mouseout", this._handleMouseOut.bind(this));
53     this._selectorElement.addEventListener("keydown", this._handleKeyDown.bind(this));
54     this._selectorElement.addEventListener("keyup", this._handleKeyUp.bind(this));
55     this._selectorElement.addEventListener("paste", this._handleSelectorPaste.bind(this));
56     this._headerElement.appendChild(this._selectorElement);
57
58     this._originElement = document.createElement("span");
59     this._originElement.className = "origin";
60     this._headerElement.appendChild(this._originElement);
61
62     this._propertiesElement = document.createElement("div");
63     this._propertiesElement.className = "properties";
64
65     this._propertiesTextEditor = new WebInspector.CSSStyleDeclarationTextEditor(this, style);
66     this._propertiesElement.appendChild(this._propertiesTextEditor.element);
67
68     this._element.appendChild(this._headerElement);
69     this._element.appendChild(this._propertiesElement);
70
71     var iconClassName;
72     switch (style.type) {
73     case WebInspector.CSSStyleDeclaration.Type.Rule:
74         console.assert(style.ownerRule);
75
76         if (style.inherited)
77             iconClassName = WebInspector.CSSStyleDeclarationSection.InheritedStyleRuleIconStyleClassName;
78         else if (style.ownerRule.type === WebInspector.CSSRule.Type.Author)
79             iconClassName = WebInspector.CSSStyleDeclarationSection.AuthorStyleRuleIconStyleClassName;
80         else if (style.ownerRule.type === WebInspector.CSSRule.Type.User)
81             iconClassName = WebInspector.CSSStyleDeclarationSection.UserStyleRuleIconStyleClassName;
82         else if (style.ownerRule.type === WebInspector.CSSRule.Type.UserAgent)
83             iconClassName = WebInspector.CSSStyleDeclarationSection.UserAgentStyleRuleIconStyleClassName;
84         else if (style.ownerRule.type === WebInspector.CSSRule.Type.Inspector)
85             iconClassName = WebInspector.CSSStyleDeclarationSection.InspectorStyleRuleIconStyleClassName;
86         break;
87
88     case WebInspector.CSSStyleDeclaration.Type.Inline:
89     case WebInspector.CSSStyleDeclaration.Type.Attribute:
90         if (style.inherited)
91             iconClassName = WebInspector.CSSStyleDeclarationSection.InheritedElementStyleRuleIconStyleClassName;
92         else
93             iconClassName = WebInspector.DOMTreeElementPathComponent.DOMElementIconStyleClassName;
94         break;
95     }
96
97     // Matches all situations except for User Agent styles.
98     if (!(style.ownerRule && style.ownerRule.type === WebInspector.CSSRule.Type.UserAgent)) {
99         this._iconElement.classList.add("toggle-able");
100         this._iconElement.title = WebInspector.UIString("Comment All Properties");
101         this._iconElement.addEventListener("click", this._toggleRuleOnOff.bind(this));
102     }
103
104     console.assert(iconClassName);
105     this._element.classList.add(iconClassName);
106
107     if (!style.editable)
108         this._element.classList.add(WebInspector.CSSStyleDeclarationSection.LockedStyleClassName);
109     else if (style.ownerRule) {
110         this._style.ownerRule.addEventListener(WebInspector.CSSRule.Event.SelectorChanged, this._markSelector.bind(this));
111         this._commitSelectorKeyboardShortcut = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Enter, this._commitSelector.bind(this), this._selectorElement);
112         this._selectorElement.addEventListener("blur", this._commitSelector.bind(this));
113     } else
114         this._element.classList.add(WebInspector.CSSStyleDeclarationSection.SelectorLockedStyleClassName);
115
116     if (!WebInspector.CSSStyleDeclarationSection._generatedLockImages) {
117         WebInspector.CSSStyleDeclarationSection._generatedLockImages = true;
118
119         var specifications = {"style-lock-normal": {fillColor: [0, 0, 0, 0.5]}};
120         generateColoredImagesForCSS("Images/Locked.svg", specifications, 8, 10);
121     }
122
123     this.refresh();
124
125     this._headerElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this));
126 };
127
128 WebInspector.CSSStyleDeclarationSection.LockedStyleClassName = "locked";
129 WebInspector.CSSStyleDeclarationSection.SelectorLockedStyleClassName = "selector-locked";
130 WebInspector.CSSStyleDeclarationSection.SelectorInvalidClassName = "invalid-selector";
131 WebInspector.CSSStyleDeclarationSection.LastInGroupStyleClassName = "last-in-group";
132 WebInspector.CSSStyleDeclarationSection.MatchedSelectorElementStyleClassName = "matched";
133
134 WebInspector.CSSStyleDeclarationSection.AuthorStyleRuleIconStyleClassName = "author-style-rule-icon";
135 WebInspector.CSSStyleDeclarationSection.UserStyleRuleIconStyleClassName = "user-style-rule-icon";
136 WebInspector.CSSStyleDeclarationSection.UserAgentStyleRuleIconStyleClassName = "user-agent-style-rule-icon";
137 WebInspector.CSSStyleDeclarationSection.InspectorStyleRuleIconStyleClassName = "inspector-style-rule-icon";
138 WebInspector.CSSStyleDeclarationSection.InheritedStyleRuleIconStyleClassName = "inherited-style-rule-icon";
139 WebInspector.CSSStyleDeclarationSection.InheritedElementStyleRuleIconStyleClassName = "inherited-element-style-rule-icon";
140
141 WebInspector.CSSStyleDeclarationSection.prototype = {
142     constructor: WebInspector.CSSStyleDeclarationSection,
143
144     // Public
145
146     get element()
147     {
148         return this._element;
149     },
150
151     get style()
152     {
153         return this._style;
154     },
155
156     get lastInGroup()
157     {
158         return this._element.classList.contains(WebInspector.CSSStyleDeclarationSection.LastInGroupStyleClassName);
159     },
160
161     set lastInGroup(last)
162     {
163         if (last)
164             this._element.classList.add(WebInspector.CSSStyleDeclarationSection.LastInGroupStyleClassName);
165         else
166             this._element.classList.remove(WebInspector.CSSStyleDeclarationSection.LastInGroupStyleClassName);
167     },
168
169     get focused()
170     {
171         return this._propertiesTextEditor.focused;
172     },
173
174     focus: function()
175     {
176         this._propertiesTextEditor.focus();
177     },
178
179     refresh: function()
180     {
181         this._selectorElement.removeChildren();
182         this._originElement.removeChildren();
183         this._selectorElements = [];
184
185         this._originElement.append(" \u2014 ");
186
187         function appendSelector(selector, matched)
188         {
189             console.assert(selector instanceof WebInspector.CSSSelector);
190
191             var selectorElement = document.createElement("span");
192             selectorElement.textContent = selector.text;
193
194             if (matched)
195                 selectorElement.className = WebInspector.CSSStyleDeclarationSection.MatchedSelectorElementStyleClassName;
196
197             var specificity = selector.specificity;
198             if (specificity) {
199                 var tooltip = WebInspector.UIString("Specificity: (%d, %d, %d)").format(specificity[0], specificity[1], specificity[2]);
200                 if (selector.dynamic) {
201                     tooltip += "\n";
202                     if (this._style.inherited)
203                         tooltip += WebInspector.UIString("Dynamically calculated for the parent element");
204                     else
205                         tooltip += WebInspector.UIString("Dynamically calculated for the selected element");
206                 }
207                 selectorElement.title = tooltip;
208             } else if (selector.dynamic) {
209                 var tooltip = WebInspector.UIString("Specificity: No value for selected element");
210                 tooltip += "\n";
211                 tooltip += WebInspector.UIString("Dynamically calculated for the selected element and did not match");
212                 selectorElement.title = tooltip;
213             }
214
215             this._selectorElement.appendChild(selectorElement);
216             this._selectorElements.push(selectorElement);
217         }
218
219         function appendSelectorTextKnownToMatch(selectorText)
220         {
221             var selectorElement = document.createElement("span");
222             selectorElement.textContent = selectorText;
223             selectorElement.className = WebInspector.CSSStyleDeclarationSection.MatchedSelectorElementStyleClassName;
224             this._selectorElement.appendChild(selectorElement);
225         }
226
227         switch (this._style.type) {
228         case WebInspector.CSSStyleDeclaration.Type.Rule:
229             console.assert(this._style.ownerRule);
230
231             var selectors = this._style.ownerRule.selectors;
232             var matchedSelectorIndices = this._style.ownerRule.matchedSelectorIndices;
233             var alwaysMatch = !matchedSelectorIndices.length;
234             if (selectors.length) {
235                 for (var i = 0; i < selectors.length; ++i) {
236                     appendSelector.call(this, selectors[i], alwaysMatch || matchedSelectorIndices.includes(i));
237                     if (i < selectors.length - 1)
238                         this._selectorElement.append(", ");
239                 }
240             } else
241                 appendSelectorTextKnownToMatch.call(this, this._style.ownerRule.selectorText);
242
243             if (this._style.ownerRule.sourceCodeLocation) {
244                 var sourceCodeLink = WebInspector.createSourceCodeLocationLink(this._style.ownerRule.sourceCodeLocation, true);
245                 this._originElement.appendChild(sourceCodeLink);
246             } else {
247                 var originString;
248                 switch (this._style.ownerRule.type) {
249                 case WebInspector.CSSRule.Type.Author:
250                     originString = WebInspector.UIString("Author Stylesheet");
251                     break;
252
253                 case WebInspector.CSSRule.Type.User:
254                     originString = WebInspector.UIString("User Stylesheet");
255                     break;
256
257                 case WebInspector.CSSRule.Type.UserAgent:
258                     originString = WebInspector.UIString("User Agent Stylesheet");
259                     break;
260
261                 case WebInspector.CSSRule.Type.Inspector:
262                     originString = WebInspector.UIString("Web Inspector");
263                     break;
264                 }
265
266                 console.assert(originString);
267                 if (originString)
268                     this._originElement.append(originString);
269             }
270
271             break;
272
273         case WebInspector.CSSStyleDeclaration.Type.Inline:
274             appendSelectorTextKnownToMatch.call(this, WebInspector.displayNameForNode(this._style.node));
275             this._originElement.append(WebInspector.UIString("Style Attribute"));
276             break;
277
278         case WebInspector.CSSStyleDeclaration.Type.Attribute:
279             appendSelectorTextKnownToMatch.call(this, WebInspector.displayNameForNode(this._style.node));
280             this._originElement.append(WebInspector.UIString("HTML Attributes"));
281             break;
282         }
283     },
284
285     highlightProperty: function(property)
286     {
287         if (this._propertiesTextEditor.highlightProperty(property)) {
288             this._element.scrollIntoView();
289             return true;
290         }
291
292         return false;
293     },
294
295     findMatchingPropertiesAndSelectors: function(needle)
296     {
297         this._element.classList.remove(WebInspector.CSSStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName, WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchingSectionHasLabelClassName);
298
299         var hasMatchingSelector = false;
300
301         for (var selectorElement of this._selectorElements) {
302             selectorElement.classList.remove(WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchSectionClassName);
303
304             if (needle && selectorElement.textContent.includes(needle)) {
305                 selectorElement.classList.add(WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchSectionClassName);
306                 hasMatchingSelector = true;
307             }
308         }
309
310         if (!needle) {
311             this._propertiesTextEditor.resetFilteredProperties();
312             return false;
313         }
314
315         var hasMatchingProperty = this._propertiesTextEditor.findMatchingProperties(needle);
316
317         if (!hasMatchingProperty && !hasMatchingSelector) {
318             this._element.classList.add(WebInspector.CSSStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName);
319             return false;
320         }
321
322         return true;
323     },
324
325     updateLayout: function()
326     {
327         this._propertiesTextEditor.updateLayout();
328     },
329
330     clearSelection: function()
331     {
332         this._propertiesTextEditor.clearSelection();
333     },
334
335     cssStyleDeclarationTextEditorFocused: function()
336     {
337         if (typeof this._delegate.cssStyleDeclarationSectionEditorFocused === "function")
338             this._delegate.cssStyleDeclarationSectionEditorFocused(this);
339     },
340
341     cssStyleDeclarationTextEditorSwitchRule: function(reverse)
342     {
343         if (!this._delegate)
344             return;
345
346         if (reverse && typeof this._delegate.cssStyleDeclarationSectionEditorPreviousRule === "function")
347             this._delegate.cssStyleDeclarationSectionEditorPreviousRule(this);
348         else if (!reverse && typeof this._delegate.cssStyleDeclarationSectionEditorNextRule === "function")
349             this._delegate.cssStyleDeclarationSectionEditorNextRule(this);
350     },
351
352     focusRuleSelector: function(reverse)
353     {
354         if (this.selectorLocked) {
355             this.focus();
356             return;
357         }
358
359         if (this.locked) {
360             this.cssStyleDeclarationTextEditorSwitchRule(reverse);
361             return;
362         }
363
364         var selection = window.getSelection();
365         selection.removeAllRanges();
366
367         this._element.scrollIntoViewIfNeeded();
368
369         var range = document.createRange();
370         range.selectNodeContents(this._selectorElement);
371         selection.addRange(range);
372     },
373
374     selectLastProperty: function()
375     {
376         this._propertiesTextEditor.selectLastProperty();
377     },
378
379     get selectorLocked()
380     {
381         return !this.locked && !this._style.ownerRule;
382     },
383
384     get locked()
385     {
386         return !this._style.editable;
387     },
388
389     // Private
390
391     get _currentSelectorText()
392     {
393         if (!this._style.ownerRule)
394             return;
395
396         var selectorText = this._selectorElement.textContent;
397         if (!selectorText || !selectorText.length)
398             selectorText = this._style.ownerRule.selectorText;
399
400         return selectorText.trim();
401     },
402
403     _handleSelectorPaste: function(event)
404     {
405         if (this._style.type === WebInspector.CSSStyleDeclaration.Type.Inline || !this._style.ownerRule)
406             return;
407
408         if (!event || !event.clipboardData)
409             return;
410
411         var data = event.clipboardData.getData("text/plain");
412         if (!data)
413             return;
414
415         function parseTextForRule(text)
416         {
417             var containsBraces = /[\{\}]/;
418             if (!containsBraces.test(text))
419                 return null;
420
421             var match = text.match(/([^{]+){(.*)}/);
422             if (!match)
423                 return null;
424
425             // If the match "body" contains braces, parse that body as if it were a rule.
426             // This will usually happen if the user includes a media query in the copied text.
427             return containsBraces.test(match[2]) ? parseTextForRule(match[2]) : match;
428         }
429
430         var match = parseTextForRule(data);
431         if (!match)
432             return;
433
434         var selector = match[1].trim();
435         this._selectorElement.textContent = selector;
436         this._style.nodeStyles.changeRule(this._style.ownerRule, selector, match[2]);
437         event.preventDefault();
438     },
439
440     _handleContextMenuEvent: function(event)
441     {
442         if (window.getSelection().toString().length)
443             return;
444
445         var contextMenu = new WebInspector.ContextMenu(event);
446
447         contextMenu.appendItem(WebInspector.UIString("Copy Rule"), function() {
448             InspectorFrontendHost.copyText(this._generateCSSRuleString());
449         }.bind(this));
450
451         contextMenu.show();
452     },
453
454     _generateCSSRuleString: function()
455     {
456         var numMediaQueries = 0;
457         var styleText = "";
458
459         if (this._style.ownerRule) {
460             var mediaList = this._style.ownerRule.mediaList;
461             if (mediaList.length) {
462                 numMediaQueries = mediaList.length;
463
464                 for (var i = numMediaQueries - 1; i >= 0; --i)
465                     styleText += "    ".repeat(numMediaQueries - i - 1) + "@media " + mediaList[i].text + " {\n";
466             }
467
468             styleText += "    ".repeat(numMediaQueries) + this._style.ownerRule.selectorText;
469         } else
470             styleText += this._selectorElement.textContent;
471
472         styleText += " {\n";
473
474         for (var property of this._style.visibleProperties) {
475             styleText += "    ".repeat(numMediaQueries + 1) + property.text.trim();
476
477             if (!styleText.endsWith(";"))
478                 styleText += ";";
479
480             styleText += "\n";
481         }
482
483         for (var i = numMediaQueries; i > 0; --i)
484             styleText += "    ".repeat(i) + "}\n";
485
486         styleText += "}";
487
488         return styleText;
489     },
490
491     _toggleRuleOnOff: function()
492     {
493         if (this._hasInvalidSelector)
494             return;
495
496         this._ruleDisabled = this._ruleDisabled ? !this._propertiesTextEditor.uncommentAllProperties() : this._propertiesTextEditor.commentAllProperties();
497         this._iconElement.title = this._ruleDisabled ? WebInspector.UIString("Uncomment All Properties") : WebInspector.UIString("Comment All Properties");
498         this._element.classList.toggle("rule-disabled", this._ruleDisabled);
499     },
500
501     _highlightNodesWithSelector: function()
502     {
503         if (!this._style.ownerRule) {
504             WebInspector.domTreeManager.highlightDOMNode(this._style.node.id);
505             return;
506         }
507
508         WebInspector.domTreeManager.highlightSelector(this._currentSelectorText, this._style.node.ownerDocument.frameIdentifier);
509     },
510
511     _hideDOMNodeHighlight: function()
512     {
513         WebInspector.domTreeManager.hideDOMNodeHighlight();
514     },
515
516     _handleMouseOver: function(event)
517     {
518         this._highlightNodesWithSelector();
519     },
520
521     _handleMouseOut: function(event)
522     {
523         this._hideDOMNodeHighlight();
524     },
525
526     _handleKeyDown: function(event)
527     {
528         if (event.keyCode !== 9) {
529             this._highlightNodesWithSelector();
530             return;
531         }
532
533         if (event.shiftKey && this._delegate && typeof this._delegate.cssStyleDeclarationSectionEditorPreviousRule === "function") {
534             event.preventDefault();
535             this._delegate.cssStyleDeclarationSectionEditorPreviousRule(this, true);
536             return;
537         }
538
539         if (!event.metaKey) {
540             event.preventDefault();
541             this.focus();
542             this._propertiesTextEditor.selectFirstProperty();
543             return;
544         }
545     },
546
547     _handleKeyUp: function(event)
548     {
549         this._highlightNodesWithSelector();
550     },
551
552     _commitSelector: function(mutations)
553     {
554         console.assert(this._style.ownerRule);
555         if (!this._style.ownerRule)
556             return;
557
558         var newSelectorText = this._selectorElement.textContent.trim();
559         if (!newSelectorText) {
560             // Revert to the current selector (by doing a refresh) since the new selector is empty.
561             this.refresh();
562             return;
563         }
564
565         this._style.ownerRule.selectorText = newSelectorText;
566     },
567
568     _markSelector: function(event)
569     {
570         var valid = event && event.data && event.data.valid;
571         this._element.classList.toggle(WebInspector.CSSStyleDeclarationSection.SelectorInvalidClassName, !valid);
572         if (valid) {
573             this._iconElement.title = this._ruleDisabled ? WebInspector.UIString("Uncomment All Properties") : WebInspector.UIString("Comment All Properties");
574             this._selectorElement.title = null;
575             this.refresh();
576             return;
577         }
578
579         this._iconElement.title = WebInspector.UIString("The selector '%s' is invalid.").format(this._selectorElement.textContent.trim());
580         this._selectorElement.title = WebInspector.UIString("Using the previous selector '%s'.").format(this._style.ownerRule.selectorText);
581         for (var i = 0; i < this._selectorElement.children.length; ++i)
582             this._selectorElement.children[i].title = null;
583     },
584
585     get _hasInvalidSelector()
586     {
587         return this._element.classList.contains(WebInspector.CSSStyleDeclarationSection.SelectorInvalidClassName);
588     }
589 };
590
591 WebInspector.CSSStyleDeclarationSection.prototype.__proto__ = WebInspector.StyleDetailsPanel.prototype;