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.
7 * @extends {WebInspector.Widget}
8 * @implements {WebInspector.TargetManager.Observer}
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);
20 this._lastReportedCount = 0;
22 WebInspector.targetManager.observeTargets(this);
24 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._renderMediaQueries.bind(this), this);
30 WebInspector.MediaQueryInspector.Section = {
36 WebInspector.MediaQueryInspector.Events = {
37 HeightUpdated: "HeightUpdated",
38 CountUpdated: "CountUpdated"
41 WebInspector.MediaQueryInspector.prototype = {
44 * @param {!WebInspector.Target} target
46 targetAdded: function(target)
48 // FIXME: adapt this to multiple targets.
51 this._cssModel = WebInspector.CSSStyleModel.fromTarget(target);
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);
62 * @param {!WebInspector.Target} target
64 targetRemoved: function(target)
66 if (WebInspector.CSSStyleModel.fromTarget(target) !== this._cssModel)
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;
76 * @param {number} offset
77 * @param {number} scale
79 setAxisTransform: function(offset, scale)
81 if (this._offset === offset && Math.abs(this._scale - scale) < 1e-8)
83 this._offset = offset;
85 this._renderMediaQueries();
89 * @param {boolean} enabled
91 setEnabled: function(enabled)
93 this._enabled = enabled;
94 this._scheduleMediaQueriesUpdate();
98 * @param {!Event} event
100 _onMediaQueryClicked: function(event)
102 var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker");
103 if (!mediaQueryMarker)
107 * @param {number} width
109 function setWidth(width)
111 WebInspector.overridesSupport.settings.deviceWidth.set(width);
112 WebInspector.overridesSupport.settings.emulateResolution.set(true);
115 var model = mediaQueryMarker._model;
116 if (model.section() === WebInspector.MediaQueryInspector.Section.Max) {
117 setWidth(model.maxWidthExpression().computedLength());
120 if (model.section() === WebInspector.MediaQueryInspector.Section.Min) {
121 setWidth(model.minWidthExpression().computedLength());
124 var currentWidth = WebInspector.overridesSupport.settings.deviceWidth.get();
125 if (currentWidth !== model.minWidthExpression().computedLength())
126 setWidth(model.minWidthExpression().computedLength());
128 setWidth(model.maxWidthExpression().computedLength());
132 * @param {!Event} event
134 _onContextMenu: function(event)
136 if (!this._cssModel || !this._cssModel.isEnabled())
139 var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker");
140 if (!mediaQueryMarker)
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]);
149 var descriptor = String.sprintf("%s:%d:%d", uiLocation.uiSourceCode.uri(), uiLocation.lineNumber + 1, uiLocation.columnNumber + 1);
150 uiLocations.set(descriptor, uiLocation);
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))));
164 * @param {!WebInspector.UILocation} location
166 _revealSourceLocation: function(location)
168 WebInspector.Revealer.reveal(location);
171 _scheduleMediaQueriesUpdate: function()
175 this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this));
178 _refetchMediaQueries: function()
180 if (!this._enabled || !this._cssModel)
181 return Promise.resolve();
183 return this._cssModel.mediaQueriesPromise()
184 .then(this._rebuildMediaQueries.bind(this))
188 * @param {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>} models
189 * @return {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>}
191 _squashAdjacentEqual: function(models)
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]);
203 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
205 _rebuildMediaQueries: function(cssMedias)
207 var queryModels = [];
208 for (var i = 0; i < cssMedias.length; ++i) {
209 var cssMedia = cssMedias[i];
210 if (!cssMedia.mediaList)
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);
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]);
227 this._cachedQueryModels = queryModels;
228 this._renderMediaQueries();
231 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model1
232 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model2
235 function compareModels(model1, model2)
237 return model1.compareTo(model2);
241 _renderMediaQueries: function()
243 if (!this._cachedQueryModels)
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();
255 active: model.active(),
257 locations: [ model.rawLocation() ]
259 markers.push(lastMarker);
263 if (markers.length !== this._lastReportedCount) {
264 this._lastReportedCount = markers.length;
265 this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.CountUpdated, markers.length);
268 if (!this.isShowing())
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);
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);
295 _zoomFactor: function()
297 return WebInspector.zoomManager.zoomFactor() / this._scale;
302 this._renderMediaQueries();
306 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model
309 _createElementFromMediaQueryModel: function(model)
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"
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;
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();
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();
344 markerElement.title = model.mediaText();
346 return markerElement;
349 __proto__: WebInspector.Widget.prototype
354 * @param {!WebInspector.CSSMedia} cssMedia
355 * @param {?WebInspector.CSSMediaQueryExpression} minWidthExpression
356 * @param {?WebInspector.CSSMediaQueryExpression} maxWidthExpression
357 * @param {boolean} active
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;
370 this._section = WebInspector.MediaQueryInspector.Section.Min;
374 * @param {!WebInspector.CSSMedia} cssMedia
375 * @param {!WebInspector.CSSMediaQuery} mediaQuery
376 * @return {?WebInspector.MediaQueryInspector.MediaQueryUIModel}
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)
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;
399 if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression))
402 return new WebInspector.MediaQueryInspector.MediaQueryUIModel(cssMedia, minWidthExpression, maxWidthExpression, mediaQuery.active());
405 WebInspector.MediaQueryInspector.MediaQueryUIModel.prototype = {
407 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
410 equals: function(other)
412 return this.compareTo(other) === 0;
416 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
419 dimensionsEqual: function(other)
421 return this.section() === other.section()
422 && (!this.minWidthExpression() || (this.minWidthExpression().computedLength() === other.minWidthExpression().computedLength()))
423 && (!this.maxWidthExpression() || (this.maxWidthExpression().computedLength() === other.maxWidthExpression().computedLength()));
427 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
430 compareTo: function(other)
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)
441 if (!myLocation && otherLocation)
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;
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();
455 * @return {!WebInspector.MediaQueryInspector.Section}
459 return this._section;
465 mediaText: function()
467 return this._cssMedia.text;
471 * @return {?WebInspector.CSSLocation}
473 rawLocation: function()
475 if (!this._rawLocation)
476 this._rawLocation = this._cssMedia.rawLocation();
477 return this._rawLocation;
481 * @return {?WebInspector.CSSMediaQueryExpression}
483 minWidthExpression: function()
485 return this._minWidthExpression;
489 * @return {?WebInspector.CSSMediaQueryExpression}
491 maxWidthExpression: function()
493 return this._maxWidthExpression;