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.View}
8 * @implements {WebInspector.TargetManager.Observer}
10 WebInspector.MediaQueryInspector = function()
12 WebInspector.View.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 = {
43 * @param {!WebInspector.Target} target
45 targetAdded: function(target)
47 // FIXME: adapt this to multiple targets.
50 this._target = target;
51 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this);
52 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this);
53 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this);
54 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this);
58 * @param {!WebInspector.Target} target
60 targetRemoved: function(target)
62 if (target !== this._target)
64 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this);
65 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this);
66 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this);
67 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this);
71 * @param {number} offset
72 * @param {number} scale
74 setAxisTransform: function(offset, scale)
76 if (this._offset === offset && Math.abs(this._scale - scale) < 1e-8)
78 this._offset = offset;
80 this._renderMediaQueries();
84 * @param {boolean} enabled
86 setEnabled: function(enabled)
88 this._enabled = enabled;
89 this._scheduleMediaQueriesUpdate();
93 * @param {!Event} event
95 _onMediaQueryClicked: function(event)
97 var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker");
98 if (!mediaQueryMarker)
102 * @param {number} width
104 function setWidth(width)
106 WebInspector.overridesSupport.settings.deviceWidth.set(width);
107 WebInspector.overridesSupport.settings.emulateResolution.set(true);
110 var model = mediaQueryMarker._model;
111 if (model.section() === WebInspector.MediaQueryInspector.Section.Max) {
112 setWidth(model.maxWidthExpression().computedLength());
115 if (model.section() === WebInspector.MediaQueryInspector.Section.Min) {
116 setWidth(model.minWidthExpression().computedLength());
119 var currentWidth = WebInspector.overridesSupport.settings.deviceWidth.get();
120 if (currentWidth !== model.minWidthExpression().computedLength())
121 setWidth(model.minWidthExpression().computedLength());
123 setWidth(model.maxWidthExpression().computedLength());
127 * @param {!Event} event
129 _onContextMenu: function(event)
131 var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker");
132 if (!mediaQueryMarker)
135 var locations = mediaQueryMarker._locations;
136 var uiLocations = new Map();
137 for (var i = 0; i < locations.length; ++i) {
138 var uiLocation = WebInspector.cssWorkspaceBinding.rawLocationToUILocation(locations[i]);
141 var descriptor = String.sprintf("%s:%d:%d", uiLocation.uiSourceCode.uri(), uiLocation.lineNumber + 1, uiLocation.columnNumber + 1);
142 uiLocations.set(descriptor, uiLocation);
145 var contextMenuItems = uiLocations.keysArray().sort();
146 var contextMenu = new WebInspector.ContextMenu(event);
147 var subMenuItem = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in source code" : "Reveal In Source Code"));
148 for (var i = 0; i < contextMenuItems.length; ++i) {
149 var title = contextMenuItems[i];
150 subMenuItem.appendItem(title, this._revealSourceLocation.bind(this, /** @type {!WebInspector.UILocation} */(uiLocations.get(title))));
156 * @param {!WebInspector.UILocation} location
158 _revealSourceLocation: function(location)
160 WebInspector.Revealer.reveal(location);
163 _scheduleMediaQueriesUpdate: function()
167 this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this));
171 * @param {!WebInspector.Throttler.FinishCallback} finishCallback
173 _refetchMediaQueries: function(finishCallback)
175 if (!this._enabled) {
181 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
182 * @this {!WebInspector.MediaQueryInspector}
184 function callback(cssMedias)
186 this._rebuildMediaQueries(cssMedias);
189 this._target.cssModel.getMediaQueries(callback.bind(this));
193 * @param {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>} models
194 * @return {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>}
196 _squashAdjacentEqual: function(models)
199 for (var i = 0; i < models.length; ++i) {
200 var last = filtered.peekLast();
201 if (!last || !last.equals(models[i]))
202 filtered.push(models[i]);
208 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
210 _rebuildMediaQueries: function(cssMedias)
212 var queryModels = [];
213 for (var i = 0; i < cssMedias.length; ++i) {
214 var cssMedia = cssMedias[i];
215 if (!cssMedia.mediaList)
217 for (var j = 0; j < cssMedia.mediaList.length; ++j) {
218 var mediaQuery = cssMedia.mediaList[j];
219 var queryModel = WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaQuery(cssMedia, mediaQuery);
220 if (queryModel && queryModel.rawLocation())
221 queryModels.push(queryModel);
224 queryModels.sort(compareModels);
225 queryModels = this._squashAdjacentEqual(queryModels);
227 var allEqual = this._cachedQueryModels && this._cachedQueryModels.length == queryModels.length;
228 for (var i = 0; allEqual && i < queryModels.length; ++i)
229 allEqual = allEqual && this._cachedQueryModels[i].equals(queryModels[i]);
232 this._cachedQueryModels = queryModels;
233 this._renderMediaQueries();
236 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model1
237 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model2
240 function compareModels(model1, model2)
242 return model1.compareTo(model2);
246 _renderMediaQueries: function()
248 if (!this._cachedQueryModels)
252 var lastMarker = null;
253 for (var i = 0; i < this._cachedQueryModels.length; ++i) {
254 var model = this._cachedQueryModels[i];
255 if (lastMarker && lastMarker.model.dimensionsEqual(model)) {
256 lastMarker.locations.push(model.rawLocation());
257 lastMarker.active = lastMarker.active || model.active();
260 active: model.active(),
262 locations: [ model.rawLocation() ]
264 markers.push(lastMarker);
268 if (markers.length !== this._lastReportedCount) {
269 this._lastReportedCount = markers.length;
270 this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.CountUpdated, markers.length);
273 if (!this.isShowing())
276 var oldChildrenCount = this.element.children.length;
277 var scrollTop = this.element.scrollTop;
278 this.element.removeChildren();
280 var container = null;
281 for (var i = 0; i < markers.length; ++i) {
282 if (!i || markers[i].model.section() !== markers[i - 1].model.section())
283 container = this.element.createChild("div", "media-inspector-marker-container");
284 var marker = markers[i];
285 var bar = this._createElementFromMediaQueryModel(marker.model);
286 bar._model = marker.model;
287 bar._locations = marker.locations;
288 bar.classList.toggle("media-inspector-marker-inactive", !marker.active);
289 container.appendChild(bar);
291 this.element.scrollTop = scrollTop;
292 this.element.classList.toggle("media-inspector-view-empty", !this.element.children.length);
293 if (this.element.children.length !== oldChildrenCount)
294 this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.HeightUpdated);
300 _zoomFactor: function()
302 return WebInspector.zoomManager.zoomFactor() / this._scale;
307 this._renderMediaQueries();
311 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model
314 _createElementFromMediaQueryModel: function(model)
316 var zoomFactor = this._zoomFactor();
317 var minWidthValue = model.minWidthExpression() ? model.minWidthExpression().computedLength() : 0;
319 const styleClassPerSection = [
320 "media-inspector-marker-max-width",
321 "media-inspector-marker-min-max-width",
322 "media-inspector-marker-min-width"
324 var markerElement = createElementWithClass("div", "media-inspector-marker");
325 var leftPixelValue = minWidthValue ? (minWidthValue - this._offset) / zoomFactor : 0;
326 markerElement.style.left = leftPixelValue + "px";
327 markerElement.classList.add(styleClassPerSection[model.section()]);
328 var widthPixelValue = null;
329 if (model.maxWidthExpression() && model.minWidthExpression())
330 widthPixelValue = (model.maxWidthExpression().computedLength() - minWidthValue) / zoomFactor;
331 else if (model.maxWidthExpression())
332 widthPixelValue = (model.maxWidthExpression().computedLength() - this._offset) / zoomFactor;
334 markerElement.style.right = "0";
335 if (typeof widthPixelValue === "number")
336 markerElement.style.width = widthPixelValue + "px";
338 if (model.minWidthExpression()) {
339 var labelClass = model.section() === WebInspector.MediaQueryInspector.Section.MinMax ? "media-inspector-label-right" : "media-inspector-label-left";
340 var labelContainer = markerElement.createChild("div", "media-inspector-marker-label-container media-inspector-marker-label-container-left");
341 labelContainer.createChild("span", "media-inspector-marker-label " + labelClass).textContent = model.minWidthExpression().value() + model.minWidthExpression().unit();
344 if (model.maxWidthExpression()) {
345 var labelClass = model.section() === WebInspector.MediaQueryInspector.Section.MinMax ? "media-inspector-label-left" : "media-inspector-label-right";
346 var labelContainer = markerElement.createChild("div", "media-inspector-marker-label-container media-inspector-marker-label-container-right");
347 labelContainer.createChild("span", "media-inspector-marker-label " + labelClass).textContent = model.maxWidthExpression().value() + model.maxWidthExpression().unit();
349 markerElement.title = model.mediaText();
351 return markerElement;
354 __proto__: WebInspector.View.prototype
359 * @param {!WebInspector.CSSMedia} cssMedia
360 * @param {?WebInspector.CSSMediaQueryExpression} minWidthExpression
361 * @param {?WebInspector.CSSMediaQueryExpression} maxWidthExpression
362 * @param {boolean} active
364 WebInspector.MediaQueryInspector.MediaQueryUIModel = function(cssMedia, minWidthExpression, maxWidthExpression, active)
366 this._cssMedia = cssMedia;
367 this._minWidthExpression = minWidthExpression;
368 this._maxWidthExpression = maxWidthExpression;
369 this._active = active;
370 if (maxWidthExpression && !minWidthExpression)
371 this._section = WebInspector.MediaQueryInspector.Section.Max;
372 else if (minWidthExpression && maxWidthExpression)
373 this._section = WebInspector.MediaQueryInspector.Section.MinMax;
375 this._section = WebInspector.MediaQueryInspector.Section.Min;
379 * @param {!WebInspector.CSSMedia} cssMedia
380 * @param {!WebInspector.CSSMediaQuery} mediaQuery
381 * @return {?WebInspector.MediaQueryInspector.MediaQueryUIModel}
383 WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaQuery = function(cssMedia, mediaQuery)
385 var maxWidthExpression = null;
386 var maxWidthPixels = Number.MAX_VALUE;
387 var minWidthExpression = null;
388 var minWidthPixels = Number.MIN_VALUE;
389 var expressions = mediaQuery.expressions();
390 for (var i = 0; i < expressions.length; ++i) {
391 var expression = expressions[i];
392 var feature = expression.feature();
393 if (feature.indexOf("width") === -1)
395 var pixels = expression.computedLength();
396 if (feature.startsWith("max-") && pixels < maxWidthPixels) {
397 maxWidthExpression = expression;
398 maxWidthPixels = pixels;
399 } else if (feature.startsWith("min-") && pixels > minWidthPixels) {
400 minWidthExpression = expression;
401 minWidthPixels = pixels;
404 if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression))
407 return new WebInspector.MediaQueryInspector.MediaQueryUIModel(cssMedia, minWidthExpression, maxWidthExpression, mediaQuery.active());
410 WebInspector.MediaQueryInspector.MediaQueryUIModel.prototype = {
412 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
415 equals: function(other)
417 return this.compareTo(other) === 0;
421 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
424 dimensionsEqual: function(other)
426 return this.section() === other.section()
427 && (!this.minWidthExpression() || (this.minWidthExpression().computedLength() === other.minWidthExpression().computedLength()))
428 && (!this.maxWidthExpression() || (this.maxWidthExpression().computedLength() === other.maxWidthExpression().computedLength()));
432 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
435 compareTo: function(other)
437 if (this.section() !== other.section())
438 return this.section() - other.section();
439 if (this.dimensionsEqual(other)) {
440 var myLocation = this.rawLocation();
441 var otherLocation = other.rawLocation();
442 if (!myLocation && !otherLocation)
443 return this.mediaText().compareTo(other.mediaText());
444 if (myLocation && !otherLocation)
446 if (!myLocation && otherLocation)
448 if (this.active() !== other.active())
449 return this.active() ? -1 : 1;
450 return myLocation.url.compareTo(otherLocation.url) || myLocation.lineNumber - otherLocation.lineNumber || myLocation.columnNumber - otherLocation.columnNumber;
452 if (this.section() === WebInspector.MediaQueryInspector.Section.Max)
453 return other.maxWidthExpression().computedLength() - this.maxWidthExpression().computedLength();
454 if (this.section() === WebInspector.MediaQueryInspector.Section.Min)
455 return this.minWidthExpression().computedLength() - other.minWidthExpression().computedLength();
456 return this.minWidthExpression().computedLength() - other.minWidthExpression().computedLength() || other.maxWidthExpression().computedLength() - this.maxWidthExpression().computedLength();
460 * @return {!WebInspector.MediaQueryInspector.Section}
464 return this._section;
470 mediaText: function()
472 return this._cssMedia.text;
476 * @return {?WebInspector.CSSLocation}
478 rawLocation: function()
480 if (!this._rawLocation)
481 this._rawLocation = this._cssMedia.rawLocation();
482 return this._rawLocation;
486 * @return {?WebInspector.CSSMediaQueryExpression}
488 minWidthExpression: function()
490 return this._minWidthExpression;
494 * @return {?WebInspector.CSSMediaQueryExpression}
496 maxWidthExpression: function()
498 return this._maxWidthExpression;