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