d4f7dbc4eefb8e4e71a85ef55710a344056caf95
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SpreadsheetCSSStyleDeclarationSection.js
1 /*
2  * Copyright (C) 2017 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 WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationSection extends WI.View
27 {
28     constructor(delegate, style)
29     {
30         console.assert(style instanceof WI.CSSStyleDeclaration, style);
31
32         let element = document.createElement("section");
33         element.classList.add("spreadsheet-css-declaration");
34
35         super(element);
36
37         this._delegate = delegate || null;
38         this._style = style;
39         this._propertiesEditor = null;
40         this._selectorElements = [];
41         this._mediaElements = [];
42         this._filterText = null;
43         this._shouldFocusSelectorElement = false;
44         this._wasEditing = false;
45
46         this._isMousePressed = false;
47         this._mouseDownIndex = NaN;
48     }
49
50     // Public
51
52     get style() { return this._style; }
53
54     get editable()
55     {
56         return this._style.editable;
57     }
58
59     initialLayout()
60     {
61         super.initialLayout();
62
63         this._headerElement = document.createElement("div");
64         this._headerElement.classList.add("header");
65
66         this._styleOriginView = new WI.StyleOriginView();
67         this._headerElement.append(this._styleOriginView.element);
68
69         this._selectorElement = document.createElement("span");
70         this._selectorElement.classList.add("selector");
71         this._selectorElement.addEventListener("mouseenter", this._highlightNodesWithSelector.bind(this));
72         this._selectorElement.addEventListener("mouseleave", this._hideDOMNodeHighlight.bind(this));
73         this._headerElement.append(this._selectorElement);
74
75         this._openBrace = document.createElement("span");
76         this._openBrace.classList.add("open-brace");
77         this._openBrace.textContent = " {";
78         this._headerElement.append(this._openBrace);
79
80         if (this._style.selectorEditable) {
81             this._selectorTextField = new WI.SpreadsheetSelectorField(this, this._selectorElement);
82             this._selectorElement.tabIndex = 0;
83             this._selectorElement.addEventListener("focus", () => this._headerElement.classList.add("editing-selector"));
84             this._selectorElement.addEventListener("blur", () => this._headerElement.classList.remove("editing-selector"));
85         }
86
87         this._propertiesEditor = new WI.SpreadsheetCSSStyleDeclarationEditor(this, this._style);
88         this._propertiesEditor.element.classList.add("properties");
89         this._propertiesEditor.addEventListener(WI.SpreadsheetCSSStyleDeclarationEditor.Event.FilterApplied, this._handleEditorFilterApplied, this);
90
91         this._closeBrace = document.createElement("span");
92         this._closeBrace.classList.add("close-brace");
93         this._closeBrace.textContent = "}";
94
95         this._element.append(this._createMediaHeader(), this._headerElement);
96         this.addSubview(this._propertiesEditor);
97         this._propertiesEditor.needsLayout();
98         this._element.append(this._closeBrace);
99
100         if (!this._style.editable)
101             this._element.classList.add("locked");
102         else if (!this._style.ownerRule)
103             this._element.classList.add("selector-locked");
104
105         this.element.addEventListener("mousedown", this._handleMouseDown.bind(this));
106
107         if (this._style.editable) {
108             this.element.addEventListener("click", this._handleClick.bind(this));
109
110             new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "S", this._save.bind(this), this._element);
111             new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "S", this._save.bind(this), this._element);
112         }
113     }
114
115     layout()
116     {
117         super.layout();
118
119         this._styleOriginView.update(this._style);
120         this._renderSelector();
121
122         if (this._shouldFocusSelectorElement)
123             this.startEditingRuleSelector();
124     }
125
126     hidden()
127     {
128         this._propertiesEditor.hidden();
129     }
130
131     startEditingRuleSelector()
132     {
133         if (!this._selectorElement) {
134             this._shouldFocusSelectorElement = true;
135             return;
136         }
137
138         this._shouldFocusSelectorElement = false;
139
140         if (this._style.selectorEditable)
141             this._selectorElement.focus();
142         else
143             this._propertiesEditor.startEditingFirstProperty();
144     }
145
146     highlightProperty(property)
147     {
148         // When navigating from the Computed panel to the Styles panel, the latter
149         // could be empty. Layout all properties so they can be highlighted.
150         if (!this.didInitialLayout)
151             this.updateLayout();
152
153         if (this._propertiesEditor.highlightProperty(property)) {
154             this._element.scrollIntoView();
155             return true;
156         }
157
158         return false;
159     }
160
161     // SpreadsheetSelectorField delegate
162
163     spreadsheetSelectorFieldDidChange(direction)
164     {
165         let selectorText = this._selectorElement.textContent.trim();
166
167         if (!selectorText || selectorText === this._style.ownerRule.selectorText)
168             this._discardSelectorChange();
169         else {
170             this._style.ownerRule.singleFireEventListener(WI.CSSRule.Event.SelectorChanged, this._renderSelector, this);
171             this._style.ownerRule.selectorText = selectorText;
172         }
173
174         if (!direction) {
175             // Don't do anything when it's a blur event.
176             return;
177         }
178
179         if (direction === "forward")
180             this._propertiesEditor.startEditingFirstProperty();
181         else if (direction === "backward") {
182             if (this._delegate.spreadsheetCSSStyleDeclarationSectionStartEditingAdjacentRule) {
183                 const delta = -1;
184                 this._delegate.spreadsheetCSSStyleDeclarationSectionStartEditingAdjacentRule(this, delta);
185             } else
186                 this._propertiesEditor.startEditingLastProperty();
187         }
188     }
189
190     spreadsheetSelectorFieldDidDiscard()
191     {
192         this._discardSelectorChange();
193     }
194
195     // SpreadsheetCSSStyleDeclarationEditor delegate
196
197     spreadsheetCSSStyleDeclarationEditorStartEditingRuleSelector()
198     {
199         this.startEditingRuleSelector();
200     }
201
202     spreadsheetCSSStyleDeclarationEditorStartEditingAdjacentRule(propertiesEditor, delta)
203     {
204         if (!this._delegate)
205             return;
206
207         if (this._delegate.spreadsheetCSSStyleDeclarationSectionStartEditingAdjacentRule)
208             this._delegate.spreadsheetCSSStyleDeclarationSectionStartEditingAdjacentRule(this, delta);
209     }
210
211     spreadsheetCSSStyleDeclarationEditorPropertyBlur(event, property)
212     {
213         if (!this._isMousePressed)
214             this._propertiesEditor.deselectProperties();
215     }
216
217     spreadsheetCSSStyleDeclarationEditorPropertyMouseEnter(event, property)
218     {
219         if (this._isMousePressed) {
220             let index = parseInt(property.element.dataset.propertyIndex);
221             this._propertiesEditor.selectProperties(this._mouseDownIndex, index);
222         }
223     }
224
225     applyFilter(filterText)
226     {
227         this._filterText = filterText;
228
229         if (!this.didInitialLayout)
230             return;
231
232         this._element.classList.remove(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName);
233
234         this._propertiesEditor.applyFilter(this._filterText);
235     }
236
237     // Private
238
239     _discardSelectorChange()
240     {
241         // Re-render selector for syntax highlighting.
242         this._renderSelector();
243     }
244
245     _renderSelector()
246     {
247         this._selectorElement.removeChildren();
248         this._selectorElements = [];
249
250         let appendSelector = (selector, matched) => {
251             console.assert(selector instanceof WI.CSSSelector);
252
253             let selectorElement = this._selectorElement.appendChild(document.createElement("span"));
254             selectorElement.textContent = selector.text;
255
256             if (matched)
257                 selectorElement.classList.add(WI.SpreadsheetCSSStyleDeclarationSection.MatchedSelectorElementStyleClassName);
258
259             if (selector.specificity) {
260                 let specificity = selector.specificity.map((number) => number.toLocaleString());
261                 let tooltip = WI.UIString("Specificity: (%d, %d, %d)").format(...specificity);
262                 if (selector.dynamic) {
263                     tooltip += "\n";
264                     if (this._style.inherited)
265                         tooltip += WI.UIString("Dynamically calculated for the parent element");
266                     else
267                         tooltip += WI.UIString("Dynamically calculated for the selected element");
268                 }
269                 selectorElement.title = tooltip;
270             } else if (selector.dynamic) {
271                 let tooltip = WI.UIString("Specificity: No value for selected element");
272                 tooltip += "\n";
273                 tooltip += WI.UIString("Dynamically calculated for the selected element and did not match");
274                 selectorElement.title = tooltip;
275             }
276
277             this._selectorElements.push(selectorElement);
278         };
279
280         let appendSelectorTextKnownToMatch = (selectorText) => {
281             let selectorElement = this._selectorElement.appendChild(document.createElement("span"));
282             selectorElement.textContent = selectorText;
283             selectorElement.classList.add(WI.SpreadsheetCSSStyleDeclarationSection.MatchedSelectorElementStyleClassName);
284         };
285
286         switch (this._style.type) {
287         case WI.CSSStyleDeclaration.Type.Rule:
288             console.assert(this._style.ownerRule);
289
290             var selectors = this._style.ownerRule.selectors;
291             var matchedSelectorIndices = this._style.ownerRule.matchedSelectorIndices;
292             var alwaysMatch = !matchedSelectorIndices.length;
293             if (selectors.length) {
294                 let hasMatchingPseudoElementSelector = false;
295                 for (let i = 0; i < selectors.length; ++i) {
296                     appendSelector(selectors[i], alwaysMatch || matchedSelectorIndices.includes(i));
297                     if (i < selectors.length - 1)
298                         this._selectorElement.append(", ");
299
300                     if (matchedSelectorIndices.includes(i) && selectors[i].isPseudoElementSelector())
301                         hasMatchingPseudoElementSelector = true;
302                 }
303                 this._element.classList.toggle("pseudo-element-selector", hasMatchingPseudoElementSelector);
304             } else
305                 appendSelectorTextKnownToMatch(this._style.ownerRule.selectorText);
306
307             break;
308
309         case WI.CSSStyleDeclaration.Type.Inline:
310             this._selectorElement.textContent = WI.UIString("Style Attribute");
311             this._selectorElement.classList.add("style-attribute");
312             break;
313
314         case WI.CSSStyleDeclaration.Type.Attribute:
315             appendSelectorTextKnownToMatch(this._style.node.displayName);
316             break;
317         }
318
319         if (this._filterText)
320             this.applyFilter(this._filterText);
321     }
322
323     _createMediaHeader()
324     {
325         let mediaList = this._style.mediaList;
326         if (!mediaList.length || (mediaList.length === 1 && (mediaList[0].text === "all" || mediaList[0].text === "screen")))
327             return "";
328
329         let mediaElement = document.createElement("div");
330         mediaElement.classList.add("header-media");
331
332         let mediaLabel = mediaElement.appendChild(document.createElement("div"));
333         mediaLabel.className = "media-label";
334         mediaLabel.append("@media ");
335
336         this._mediaElements = mediaList.map((media, i) => {
337             if (i)
338                 mediaLabel.append(", ");
339
340             let span = mediaLabel.appendChild(document.createElement("span"));
341             span.textContent = media.text;
342             return span;
343         });
344
345         return mediaElement;
346     }
347
348     _save(event)
349     {
350         event.stop();
351
352         if (this._style.type !== WI.CSSStyleDeclaration.Type.Rule) {
353             // FIXME: Can't save CSS inside <style></style> <https://webkit.org/b/150357>
354             InspectorFrontendHost.beep();
355             return;
356         }
357
358         console.assert(this._style.ownerRule instanceof WI.CSSRule);
359         console.assert(this._style.ownerRule.sourceCodeLocation instanceof WI.SourceCodeLocation);
360
361         let sourceCode = this._style.ownerRule.sourceCodeLocation.sourceCode;
362         if (sourceCode.type !== WI.Resource.Type.Stylesheet) {
363             // FIXME: Can't save CSS inside style="" <https://webkit.org/b/150357>
364             InspectorFrontendHost.beep();
365             return;
366         }
367
368         let url;
369         if (sourceCode.urlComponents.scheme === "data") {
370             let mainResource = WI.networkManager.mainFrame.mainResource;
371             if (mainResource.urlComponents.lastPathComponent.endsWith(".html"))
372                 url = mainResource.url.replace(/\.html$/, "-data.css");
373             else {
374                 let pathDirectory = mainResource.url.slice(0, -mainResource.urlComponents.lastPathComponent.length);
375                 url = pathDirectory + "data.css";
376             }
377         } else
378             url = sourceCode.url;
379
380         const saveAs = event.shiftKey;
381         WI.FileUtilities.save({url: url, content: sourceCode.content}, saveAs);
382     }
383
384     _handleMouseDown(event)
385     {
386         this._wasEditing = this._propertiesEditor.editing || document.activeElement === this._selectorElement;
387
388         let propertyElement = event.target.closest(".property");
389         if (!propertyElement)
390             return;
391
392         this._isMousePressed = true;
393
394         // Disable text selection on mousemove.
395         event.preventDefault();
396
397         // Canceling mousedown event prevents blur event from firing on the previously focused element.
398         if (this._wasEditing && document.activeElement)
399             document.activeElement.blur();
400
401         // Prevent name/value fields from editing when properties selected.
402         window.addEventListener("click", this._handleWindowClick.bind(this), {capture: true, once: true});
403
404         let propertyIndex = parseInt(propertyElement.dataset.propertyIndex);
405         if (event.shiftKey && this._propertiesEditor.hasSelectedProperties())
406             this._propertiesEditor.extendSelectedProperties(propertyIndex);
407         else
408             this._propertiesEditor.deselectProperties();
409
410         this._mouseDownIndex = propertyIndex;
411         this._element.classList.add("selecting");
412     }
413
414     _handleWindowClick(event)
415     {
416         if (this._propertiesEditor.hasSelectedProperties()) {
417             // Don't start editing name/value if there's selection.
418             event.stop();
419         }
420
421         this._isMousePressed = false;
422         this._mouseDownIndex = NaN;
423
424         this._element.classList.remove("selecting");
425     }
426
427     _handleClick(event)
428     {
429         if (this._wasEditing || this._propertiesEditor.hasSelectedProperties())
430             return;
431
432         if (window.getSelection().type === "Range")
433             return;
434
435         event.stop();
436
437         if (event.target.classList.contains(WI.SpreadsheetStyleProperty.StyleClassName)) {
438             let propertyIndex = parseInt(event.target.dataset.propertyIndex);
439             this._propertiesEditor.addBlankProperty(propertyIndex + 1);
440             return;
441         }
442
443         if (event.target === this._headerElement || event.target === this._openBrace) {
444             this._propertiesEditor.addBlankProperty(0);
445             return;
446         }
447
448         if (event.target === this._element || event.target === this._closeBrace) {
449             const appendAfterLast = -1;
450             this._propertiesEditor.addBlankProperty(appendAfterLast);
451         }
452     }
453
454     _highlightNodesWithSelector()
455     {
456         if (!this._style.ownerRule) {
457             WI.domManager.highlightDOMNode(this._style.node.id);
458             return;
459         }
460
461         let selectorText = this._selectorElement.textContent.trim();
462         WI.domManager.highlightSelector(selectorText, this._style.node.ownerDocument.frameIdentifier);
463     }
464
465     _hideDOMNodeHighlight()
466     {
467         WI.domManager.hideDOMNodeHighlight();
468     }
469
470     _handleEditorFilterApplied(event)
471     {
472         let matchesMedia = false;
473         for (let mediaElement of this._mediaElements) {
474             mediaElement.classList.remove(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName);
475
476             if (mediaElement.textContent.includes(this._filterText)) {
477                 mediaElement.classList.add(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName);
478                 matchesMedia = true;
479             }
480         }
481
482         let matchesSelector = false;
483         for (let selectorElement of this._selectorElements) {
484             selectorElement.classList.remove(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName);
485
486             if (selectorElement.textContent.includes(this._filterText)) {
487                 selectorElement.classList.add(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName);
488                 matchesSelector = true;
489             }
490         }
491
492         let matches = event.data.matches || matchesMedia || matchesSelector;
493         if (!matches)
494             this._element.classList.add(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName);
495
496         this.dispatchEventToListeners(WI.SpreadsheetCSSStyleDeclarationSection.Event.FilterApplied, {matches});
497     }
498 };
499
500 WI.SpreadsheetCSSStyleDeclarationSection.Event = {
501     FilterApplied: "spreadsheet-css-style-declaration-section-filter-applied",
502 };
503
504 WI.SpreadsheetCSSStyleDeclarationSection.MatchedSelectorElementStyleClassName = "matched";