Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / emulation / MediaQueryInspector.js
blob6624d38a1b3c416f3249bc8b0f042e43c7dc6c91
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @constructor
7  * @extends {WebInspector.Widget}
8  * @implements {WebInspector.TargetManager.Observer}
9  */
10 WebInspector.MediaQueryInspector = function()
12     WebInspector.Widget.call(this);
13     this.element.classList.add("media-inspector-view", "media-inspector-view-empty");
14     this.element.addEventListener("click", this._onMediaQueryClicked.bind(this), false);
15     this.element.addEventListener("contextmenu", this._onContextMenu.bind(this), false);
16     this._mediaThrottler = new WebInspector.Throttler(0);
18     this._offset = 0;
19     this._scale = 1;
20     this._lastReportedCount = 0;
22     WebInspector.targetManager.observeTargets(this);
24     WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._renderMediaQueries.bind(this), this);
27 /**
28  * @enum {number}
29  */
30 WebInspector.MediaQueryInspector.Section = {
31     Max: 0,
32     MinMax: 1,
33     Min: 2
36 WebInspector.MediaQueryInspector.Events = {
37     HeightUpdated: "HeightUpdated",
38     CountUpdated: "CountUpdated"
41 WebInspector.MediaQueryInspector.prototype = {
42     /**
43      * @override
44      * @param {!WebInspector.Target} target
45      */
46     targetAdded: function(target)
47     {
48         // FIXME: adapt this to multiple targets.
49         if (this._cssModel)
50             return;
51         this._cssModel = WebInspector.CSSStyleModel.fromTarget(target);
52         if (!this._cssModel)
53             return;
54         this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this);
55         this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this);
56         this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this);
57         this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this);
58     },
60     /**
61      * @override
62      * @param {!WebInspector.Target} target
63      */
64     targetRemoved: function(target)
65     {
66         if (WebInspector.CSSStyleModel.fromTarget(target) !== this._cssModel)
67             return;
68         this._cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this);
69         this._cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this);
70         this._cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this);
71         this._cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this);
72         delete this._cssModel;
73     },
75     /**
76      * @param {number} offset
77      * @param {number} scale
78      */
79     setAxisTransform: function(offset, scale)
80     {
81         if (this._offset === offset && Math.abs(this._scale - scale) < 1e-8)
82             return;
83         this._offset = offset;
84         this._scale = scale;
85         this._renderMediaQueries();
86     },
88     /**
89      * @param {boolean} enabled
90      */
91     setEnabled: function(enabled)
92     {
93         this._enabled = enabled;
94         this._scheduleMediaQueriesUpdate();
95     },
97     /**
98      * @param {!Event} event
99      */
100     _onMediaQueryClicked: function(event)
101     {
102         var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker");
103         if (!mediaQueryMarker)
104             return;
106         /**
107          * @param {number} width
108          */
109         function setWidth(width)
110         {
111             WebInspector.overridesSupport.settings.deviceWidth.set(width);
112             WebInspector.overridesSupport.settings.emulateResolution.set(true);
113         }
115         var model = mediaQueryMarker._model;
116         if (model.section() === WebInspector.MediaQueryInspector.Section.Max) {
117             setWidth(model.maxWidthExpression().computedLength());
118             return;
119         }
120         if (model.section() === WebInspector.MediaQueryInspector.Section.Min) {
121             setWidth(model.minWidthExpression().computedLength());
122             return;
123         }
124         var currentWidth = WebInspector.overridesSupport.settings.deviceWidth.get();
125         if (currentWidth !== model.minWidthExpression().computedLength())
126             setWidth(model.minWidthExpression().computedLength());
127         else
128             setWidth(model.maxWidthExpression().computedLength());
129     },
131     /**
132      * @param {!Event} event
133      */
134     _onContextMenu: function(event)
135     {
136         if (!this._cssModel || !this._cssModel.isEnabled())
137             return;
139         var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker");
140         if (!mediaQueryMarker)
141             return;
143         var locations = mediaQueryMarker._locations;
144         var uiLocations = new Map();
145         for (var i = 0; i < locations.length; ++i) {
146             var uiLocation = WebInspector.cssWorkspaceBinding.rawLocationToUILocation(locations[i]);
147             if (!uiLocation)
148                 continue;
149             var descriptor = String.sprintf("%s:%d:%d", uiLocation.uiSourceCode.uri(), uiLocation.lineNumber + 1, uiLocation.columnNumber + 1);
150             uiLocations.set(descriptor, uiLocation);
151         }
153         var contextMenuItems = uiLocations.keysArray().sort();
154         var contextMenu = new WebInspector.ContextMenu(event);
155         var subMenuItem = contextMenu.appendSubMenuItem(WebInspector.UIString.capitalize("Reveal in ^source ^code"));
156         for (var i = 0; i < contextMenuItems.length; ++i) {
157             var title = contextMenuItems[i];
158             subMenuItem.appendItem(title, this._revealSourceLocation.bind(this, /** @type {!WebInspector.UILocation} */(uiLocations.get(title))));
159         }
160         contextMenu.show();
161     },
163     /**
164      * @param {!WebInspector.UILocation} location
165      */
166     _revealSourceLocation: function(location)
167     {
168         WebInspector.Revealer.reveal(location);
169     },
171     _scheduleMediaQueriesUpdate: function()
172     {
173         if (!this._enabled)
174             return;
175         this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this));
176     },
178     _refetchMediaQueries: function()
179     {
180         if (!this._enabled || !this._cssModel)
181             return Promise.resolve();
183         return this._cssModel.mediaQueriesPromise()
184             .then(this._rebuildMediaQueries.bind(this))
185     },
187     /**
188      * @param {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>} models
189      * @return {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>}
190      */
191     _squashAdjacentEqual: function(models)
192     {
193         var filtered = [];
194         for (var i = 0; i < models.length; ++i) {
195             var last = filtered.peekLast();
196             if (!last || !last.equals(models[i]))
197                 filtered.push(models[i]);
198         }
199         return filtered;
200     },
202     /**
203      * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
204      */
205     _rebuildMediaQueries: function(cssMedias)
206     {
207         var queryModels = [];
208         for (var i = 0; i < cssMedias.length; ++i) {
209             var cssMedia = cssMedias[i];
210             if (!cssMedia.mediaList)
211                 continue;
212             for (var j = 0; j < cssMedia.mediaList.length; ++j) {
213                 var mediaQuery = cssMedia.mediaList[j];
214                 var queryModel = WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaQuery(cssMedia, mediaQuery);
215                 if (queryModel && queryModel.rawLocation())
216                     queryModels.push(queryModel);
217             }
218         }
219         queryModels.sort(compareModels);
220         queryModels = this._squashAdjacentEqual(queryModels);
222         var allEqual = this._cachedQueryModels && this._cachedQueryModels.length == queryModels.length;
223         for (var i = 0; allEqual && i < queryModels.length; ++i)
224             allEqual = allEqual && this._cachedQueryModels[i].equals(queryModels[i]);
225         if (allEqual)
226             return;
227         this._cachedQueryModels = queryModels;
228         this._renderMediaQueries();
230         /**
231          * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model1
232          * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model2
233          * @return {number}
234          */
235         function compareModels(model1, model2)
236         {
237             return model1.compareTo(model2);
238         }
239     },
241     _renderMediaQueries: function()
242     {
243         if (!this._cachedQueryModels)
244             return;
246         var markers = [];
247         var lastMarker = null;
248         for (var i = 0; i < this._cachedQueryModels.length; ++i) {
249             var model = this._cachedQueryModels[i];
250             if (lastMarker && lastMarker.model.dimensionsEqual(model)) {
251                 lastMarker.locations.push(model.rawLocation());
252                 lastMarker.active = lastMarker.active || model.active();
253             } else {
254                 lastMarker = {
255                     active: model.active(),
256                     model: model,
257                     locations: [ model.rawLocation() ]
258                 };
259                 markers.push(lastMarker);
260             }
261         }
263         if (markers.length !== this._lastReportedCount) {
264             this._lastReportedCount = markers.length;
265             this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.CountUpdated, markers.length);
266         }
268         if (!this.isShowing())
269             return;
271         var oldChildrenCount = this.element.children.length;
272         var scrollTop = this.element.scrollTop;
273         this.element.removeChildren();
275         var container = null;
276         for (var i = 0; i < markers.length; ++i) {
277             if (!i || markers[i].model.section() !== markers[i - 1].model.section())
278                 container = this.element.createChild("div", "media-inspector-marker-container");
279             var marker = markers[i];
280             var bar = this._createElementFromMediaQueryModel(marker.model);
281             bar._model = marker.model;
282             bar._locations = marker.locations;
283             bar.classList.toggle("media-inspector-marker-inactive", !marker.active);
284             container.appendChild(bar);
285         }
286         this.element.scrollTop = scrollTop;
287         this.element.classList.toggle("media-inspector-view-empty", !this.element.children.length);
288         if (this.element.children.length !== oldChildrenCount)
289             this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.HeightUpdated);
290     },
292     /**
293      * @return {number}
294      */
295     _zoomFactor: function()
296     {
297         return WebInspector.zoomManager.zoomFactor() / this._scale;
298     },
300     wasShown: function()
301     {
302         this._renderMediaQueries();
303     },
305     /**
306      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model
307      * @return {!Element}
308      */
309     _createElementFromMediaQueryModel: function(model)
310     {
311         var zoomFactor = this._zoomFactor();
312         var minWidthValue = model.minWidthExpression() ? model.minWidthExpression().computedLength() : 0;
314         const styleClassPerSection = [
315             "media-inspector-marker-max-width",
316             "media-inspector-marker-min-max-width",
317             "media-inspector-marker-min-width"
318         ];
319         var markerElement = createElementWithClass("div", "media-inspector-marker");
320         var leftPixelValue = minWidthValue ? (minWidthValue - this._offset) / zoomFactor : 0;
321         markerElement.style.left = leftPixelValue + "px";
322         markerElement.classList.add(styleClassPerSection[model.section()]);
323         var widthPixelValue = null;
324         if (model.maxWidthExpression() && model.minWidthExpression())
325             widthPixelValue = (model.maxWidthExpression().computedLength() - minWidthValue) / zoomFactor;
326         else if (model.maxWidthExpression())
327             widthPixelValue = (model.maxWidthExpression().computedLength() - this._offset) / zoomFactor;
328         else
329             markerElement.style.right = "0";
330         if (typeof widthPixelValue === "number")
331             markerElement.style.width = widthPixelValue + "px";
333         if (model.minWidthExpression()) {
334             var labelClass = model.section() === WebInspector.MediaQueryInspector.Section.MinMax ? "media-inspector-label-right" : "media-inspector-label-left";
335             var labelContainer = markerElement.createChild("div", "media-inspector-marker-label-container media-inspector-marker-label-container-left");
336             labelContainer.createChild("span", "media-inspector-marker-label " + labelClass).textContent = model.minWidthExpression().value() + model.minWidthExpression().unit();
337         }
339         if (model.maxWidthExpression()) {
340             var labelClass = model.section() === WebInspector.MediaQueryInspector.Section.MinMax ? "media-inspector-label-left" : "media-inspector-label-right";
341             var labelContainer = markerElement.createChild("div", "media-inspector-marker-label-container media-inspector-marker-label-container-right");
342             labelContainer.createChild("span", "media-inspector-marker-label " + labelClass).textContent = model.maxWidthExpression().value() + model.maxWidthExpression().unit();
343         }
344         markerElement.title = model.mediaText();
346         return markerElement;
347     },
349     __proto__: WebInspector.Widget.prototype
353  * @constructor
354  * @param {!WebInspector.CSSMedia} cssMedia
355  * @param {?WebInspector.CSSMediaQueryExpression} minWidthExpression
356  * @param {?WebInspector.CSSMediaQueryExpression} maxWidthExpression
357  * @param {boolean} active
358  */
359 WebInspector.MediaQueryInspector.MediaQueryUIModel = function(cssMedia, minWidthExpression, maxWidthExpression, active)
361     this._cssMedia = cssMedia;
362     this._minWidthExpression = minWidthExpression;
363     this._maxWidthExpression = maxWidthExpression;
364     this._active = active;
365     if (maxWidthExpression && !minWidthExpression)
366         this._section = WebInspector.MediaQueryInspector.Section.Max;
367     else if (minWidthExpression && maxWidthExpression)
368         this._section = WebInspector.MediaQueryInspector.Section.MinMax;
369     else
370         this._section = WebInspector.MediaQueryInspector.Section.Min;
374  * @param {!WebInspector.CSSMedia} cssMedia
375  * @param {!WebInspector.CSSMediaQuery} mediaQuery
376  * @return {?WebInspector.MediaQueryInspector.MediaQueryUIModel}
377  */
378 WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaQuery = function(cssMedia, mediaQuery)
380     var maxWidthExpression = null;
381     var maxWidthPixels = Number.MAX_VALUE;
382     var minWidthExpression = null;
383     var minWidthPixels = Number.MIN_VALUE;
384     var expressions = mediaQuery.expressions();
385     for (var i = 0; i < expressions.length; ++i) {
386         var expression = expressions[i];
387         var feature = expression.feature();
388         if (feature.indexOf("width") === -1)
389             continue;
390         var pixels = expression.computedLength();
391         if (feature.startsWith("max-") && pixels < maxWidthPixels) {
392             maxWidthExpression = expression;
393             maxWidthPixels = pixels;
394         } else if (feature.startsWith("min-") && pixels > minWidthPixels) {
395             minWidthExpression = expression;
396             minWidthPixels = pixels;
397         }
398     }
399     if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression))
400         return null;
402     return new WebInspector.MediaQueryInspector.MediaQueryUIModel(cssMedia, minWidthExpression, maxWidthExpression, mediaQuery.active());
405 WebInspector.MediaQueryInspector.MediaQueryUIModel.prototype = {
406     /**
407      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
408      * @return {boolean}
409      */
410     equals: function(other)
411     {
412         return this.compareTo(other) === 0;
413     },
415     /**
416      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
417      * @return {boolean}
418      */
419     dimensionsEqual: function(other)
420     {
421         return this.section() === other.section()
422             && (!this.minWidthExpression() || (this.minWidthExpression().computedLength() === other.minWidthExpression().computedLength()))
423             && (!this.maxWidthExpression() || (this.maxWidthExpression().computedLength() === other.maxWidthExpression().computedLength()));
424     },
426     /**
427      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
428      * @return {number}
429      */
430     compareTo: function(other)
431     {
432         if (this.section() !== other.section())
433             return this.section() - other.section();
434         if (this.dimensionsEqual(other)) {
435             var myLocation = this.rawLocation();
436             var otherLocation = other.rawLocation();
437             if (!myLocation && !otherLocation)
438                 return this.mediaText().compareTo(other.mediaText());
439             if (myLocation && !otherLocation)
440                 return 1;
441             if (!myLocation && otherLocation)
442                 return -1;
443             if (this.active() !== other.active())
444                 return this.active() ? -1 : 1;
445             return myLocation.url.compareTo(otherLocation.url) || myLocation.lineNumber - otherLocation.lineNumber || myLocation.columnNumber - otherLocation.columnNumber;
446         }
447         if (this.section() === WebInspector.MediaQueryInspector.Section.Max)
448             return other.maxWidthExpression().computedLength() - this.maxWidthExpression().computedLength();
449         if (this.section() === WebInspector.MediaQueryInspector.Section.Min)
450             return this.minWidthExpression().computedLength() - other.minWidthExpression().computedLength();
451         return this.minWidthExpression().computedLength() - other.minWidthExpression().computedLength() || other.maxWidthExpression().computedLength() - this.maxWidthExpression().computedLength();
452     },
454     /**
455      * @return {!WebInspector.MediaQueryInspector.Section}
456      */
457     section: function()
458     {
459         return this._section;
460     },
462     /**
463      * @return {string}
464      */
465     mediaText: function()
466     {
467         return this._cssMedia.text;
468     },
470     /**
471      * @return {?WebInspector.CSSLocation}
472      */
473     rawLocation: function()
474     {
475         if (!this._rawLocation)
476             this._rawLocation = this._cssMedia.rawLocation();
477         return this._rawLocation;
478     },
480     /**
481      * @return {?WebInspector.CSSMediaQueryExpression}
482      */
483     minWidthExpression: function()
484     {
485         return this._minWidthExpression;
486     },
488     /**
489      * @return {?WebInspector.CSSMediaQueryExpression}
490      */
491     maxWidthExpression: function()
492     {
493         return this._maxWidthExpression;
494     },
496     /**
497      * @return {boolean}
498      */
499     active: function()
500     {
501         return this._active;
502     }