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.element.addEventListener("webkitAnimationEnd", this._onAnimationEnd.bind(this), false);
17 this._mediaThrottler = new WebInspector.Throttler(100);
19 this._translateZero = 0;
23 this._rulerDecorationLayer = document.createElementWithClass("div", "fill");
24 this._rulerDecorationLayer.classList.add("media-inspector-ruler-decoration");
25 this._rulerDecorationLayer.addEventListener("click", this._onRulerDecorationClicked.bind(this), false);
27 WebInspector.targetManager.observeTargets(this);
29 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._renderMediaQueries.bind(this), this);
35 WebInspector.MediaQueryInspector.Section = {
41 WebInspector.MediaQueryInspector.Events = {
42 HeightUpdated: "HeightUpdated"
45 WebInspector.MediaQueryInspector.prototype = {
47 * @param {!WebInspector.Target} target
49 targetAdded: function(target)
51 // FIXME: adapt this to multiple targets.
54 this._target = target;
55 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this);
56 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this);
57 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this);
58 target.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this);
62 * @param {!WebInspector.Target} target
64 targetRemoved: function(target)
66 if (target !== this._target)
68 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this);
69 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this);
70 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this);
71 target.cssModel.removeEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this);
77 rulerDecorationLayer: function()
79 return this._rulerDecorationLayer;
83 * @return {!Array.<number>}
85 _mediaQueryThresholds: function()
87 if (!this._cachedQueryModels)
90 for (var i = 0; i < this._cachedQueryModels.length; ++i) {
91 var model = this._cachedQueryModels[i];
92 if (model.minWidthExpression())
93 thresholds.push(model.minWidthExpression().computedLength());
94 if (model.maxWidthExpression())
95 thresholds.push(model.maxWidthExpression().computedLength());
97 thresholds.sortNumbers();
102 * @param {!Event} event
104 _onRulerDecorationClicked: function(event)
106 var thresholdElement = event.target.enclosingNodeOrSelfWithClass("media-inspector-threshold-serif");
107 if (!thresholdElement)
109 WebInspector.settings.showMediaQueryInspector.set(true);
110 var revealValue = thresholdElement._value;
111 for (var mediaQueryContainer = this.element.firstChild; mediaQueryContainer; mediaQueryContainer = mediaQueryContainer.nextSibling) {
112 var model = mediaQueryContainer._model;
113 if ((model.minWidthExpression() && Math.abs(model.minWidthExpression().computedLength() - revealValue) === 0)
114 || (model.maxWidthExpression() && Math.abs(model.maxWidthExpression().computedLength() - revealValue) === 0)) {
115 mediaQueryContainer.scrollIntoViewIfNeeded(false);
116 var hasRunningAnimation = mediaQueryContainer.classList.contains("media-inspector-marker-highlight-1") || mediaQueryContainer.classList.contains("media-inspector-marker-highlight-2");
117 mediaQueryContainer.classList.toggle("media-inspector-marker-highlight-1");
118 if (hasRunningAnimation)
119 mediaQueryContainer.classList.toggle("media-inspector-marker-highlight-2");
126 * @param {!Event} event
128 _onAnimationEnd: function(event)
130 event.target.classList.remove("media-inspector-marker-highlight-1");
131 event.target.classList.remove("media-inspector-marker-highlight-2");
135 * @param {number} translate
136 * @param {number} offset
137 * @param {number} scale
139 setAxisTransform: function(translate, offset, scale)
141 if (this._translateZero === translate && this._offset === offset && Math.abs(this._scale - scale) < 1e-8)
143 this._translateZero = translate;
144 this._offset = offset;
146 this._renderMediaQueries();
150 * @param {boolean} enabled
152 setEnabled: function(enabled)
154 this._enabled = enabled;
155 this._scheduleMediaQueriesUpdate();
159 * @param {!Event} event
161 _onMediaQueryClicked: function(event)
163 var mediaQueryMarkerContainer = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker-container");
164 if (!mediaQueryMarkerContainer)
168 * @param {number} width
170 function setWidth(width)
172 WebInspector.overridesSupport.settings.deviceWidth.set(width);
173 WebInspector.overridesSupport.settings.emulateResolution.set(true);
176 var model = mediaQueryMarkerContainer._model;
177 if (model.section() === WebInspector.MediaQueryInspector.Section.Max) {
178 setWidth(model.maxWidthExpression().computedLength());
181 if (model.section() === WebInspector.MediaQueryInspector.Section.Min) {
182 setWidth(model.minWidthExpression().computedLength());
185 var currentWidth = WebInspector.overridesSupport.settings.deviceWidth.get();
186 if (currentWidth !== model.minWidthExpression().computedLength())
187 setWidth(model.minWidthExpression().computedLength());
189 setWidth(model.maxWidthExpression().computedLength());
193 * @param {!Event} event
195 _onContextMenu: function(event)
197 var mediaQueryMarkerContainer = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker-container");
198 if (!mediaQueryMarkerContainer)
201 var locations = mediaQueryMarkerContainer._locations;
202 var contextMenu = new WebInspector.ContextMenu(event);
203 var subMenuItem = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in source code" : "Reveal In Source Code"));
204 for (var i = 0; i < locations.length; ++i) {
205 var location = locations[i];
206 var title = String.sprintf("%s:%d:%d", location.uiSourceCode.uri(), location.lineNumber + 1, location.columnNumber + 1);
207 subMenuItem.appendItem(title, this._revealSourceLocation.bind(this, location));
213 * @param {!WebInspector.UILocation} location
215 _revealSourceLocation: function(location)
217 WebInspector.Revealer.reveal(location);
220 _scheduleMediaQueriesUpdate: function()
224 this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this));
228 * @param {!WebInspector.Throttler.FinishCallback} finishCallback
230 _refetchMediaQueries: function(finishCallback)
232 if (!this._enabled) {
238 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
239 * @this {!WebInspector.MediaQueryInspector}
241 function callback(cssMedias)
243 this._rebuildMediaQueries(cssMedias);
246 this._target.cssModel.getMediaQueries(callback.bind(this));
250 * @param {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>} models
251 * @return {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>}
253 _squashAdjacentEqual: function(models)
256 for (var i = 0; i < models.length; ++i) {
257 var last = filtered.peekLast();
258 if (!last || !last.equals(models[i]))
259 filtered.push(models[i]);
265 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
267 _rebuildMediaQueries: function(cssMedias)
269 var queryModels = [];
270 for (var i = 0; i < cssMedias.length; ++i) {
271 var cssMedia = cssMedias[i];
272 if (!cssMedia.mediaList)
274 for (var j = 0; j < cssMedia.mediaList.length; ++j) {
275 var mediaQueryExpressions = cssMedia.mediaList[j];
276 var queryModel = WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaExpressions(cssMedia, mediaQueryExpressions);
278 queryModels.push(queryModel);
281 queryModels.sort(compareModels);
282 queryModels = this._squashAdjacentEqual(queryModels);
284 var allEqual = this._cachedQueryModels && this._cachedQueryModels.length == queryModels.length;
285 for (var i = 0; allEqual && i < queryModels.length; ++i)
286 allEqual = allEqual && this._cachedQueryModels[i].equals(queryModels[i]);
289 this._cachedQueryModels = queryModels;
290 this._renderMediaQueries();
293 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model1
294 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model2
297 function compareModels(model1, model2)
299 return model1.compareTo(model2);
303 _renderMediaQueries: function()
305 if (!this._cachedQueryModels)
307 this._renderRulerDecorations();
308 if (!this.isShowing())
312 var lastMarker = null;
313 for (var i = 0; i < this._cachedQueryModels.length; ++i) {
314 var model = this._cachedQueryModels[i];
315 if (!model.uiLocation())
317 if (lastMarker && lastMarker.model.dimensionsEqual(model)) {
318 lastMarker.locations.push(model.uiLocation());
322 locations: [ model.uiLocation() ]
324 markers.push(lastMarker);
327 var heightChanges = this.element.children.length !== markers.length;
329 var scrollTop = this.element.scrollTop;
330 this.element.removeChildren();
331 for (var i = 0; i < markers.length; ++i) {
332 var marker = markers[i];
333 var bar = this._createElementFromMediaQueryModel(marker.model);
334 bar._model = marker.model;
335 bar._locations = marker.locations;
336 this.element.appendChild(bar);
338 this.element.scrollTop = scrollTop;
339 this.element.classList.toggle("media-inspector-view-empty", !this.element.children.length);
341 this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.HeightUpdated);
347 _zoomFactor: function()
349 return WebInspector.zoomManager.zoomFactor() / this._scale;
352 _renderRulerDecorations: function()
354 this._rulerDecorationLayer.removeChildren();
355 var zoomFactor = this._zoomFactor();
357 var thresholds = this._mediaQueryThresholds();
358 for (var i = 0; i < thresholds.length; ++i) {
359 var thresholdElement = this._rulerDecorationLayer.createChild("div", "media-inspector-threshold-serif");
360 thresholdElement._value = thresholds[i];
361 thresholdElement.style.left = (thresholds[i] - this._offset) / zoomFactor + "px";
367 this._renderMediaQueries();
371 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model
374 _createElementFromMediaQueryModel: function(model)
376 var zoomFactor = this._zoomFactor();
377 var minWidthValue = model.minWidthExpression() ? model.minWidthExpression().computedLength() : 0;
379 const styleClassPerSection = [
380 "media-inspector-marker-container-max-width",
381 "media-inspector-marker-container-min-max-width",
382 "media-inspector-marker-container-min-width"
384 var container = document.createElementWithClass("div", "media-inspector-marker-container hbox");
385 container.classList.add(styleClassPerSection[model.section()]);
387 var markerElement = container.createChild("div", "media-inspector-marker");
388 var leftPixelValue = minWidthValue ? (minWidthValue - this._offset) / zoomFactor + this._translateZero : 0;
389 markerElement.style.left = leftPixelValue + "px";
390 var widthPixelValue = null;
391 if (model.maxWidthExpression() && model.minWidthExpression())
392 widthPixelValue = (model.maxWidthExpression().computedLength() - minWidthValue) / zoomFactor;
393 else if (model.maxWidthExpression())
394 widthPixelValue = (model.maxWidthExpression().computedLength() - this._offset) / zoomFactor + this._translateZero;
396 markerElement.style.right = "0";
397 if (typeof widthPixelValue === "number")
398 markerElement.style.width = widthPixelValue + "px";
400 var maxLabelFiller = container.createChild("div", "media-inspector-max-label-filler");
401 if (model.maxWidthExpression()) {
402 maxLabelFiller.style.maxWidth = Math.max(widthPixelValue + leftPixelValue, 0) + "px";
403 var label = container.createChild("span", "media-inspector-marker-label media-inspector-max-label");
404 label.textContent = model.maxWidthExpression().computedLength() + "px";
407 if (model.minWidthExpression()) {
408 var minLabelFiller = maxLabelFiller.createChild("div", "media-inspector-min-label-filler");
409 minLabelFiller.style.maxWidth = Math.max(leftPixelValue, 0) + "px";
410 var label = minLabelFiller.createChild("span", "media-inspector-marker-label media-inspector-min-label");
411 label.textContent = model.minWidthExpression().computedLength() + "px";
417 __proto__: WebInspector.View.prototype
422 * @param {!WebInspector.CSSMedia} cssMedia
423 * @param {?WebInspector.CSSMediaQueryExpression} minWidthExpression
424 * @param {?WebInspector.CSSMediaQueryExpression} maxWidthExpression
426 WebInspector.MediaQueryInspector.MediaQueryUIModel = function(cssMedia, minWidthExpression, maxWidthExpression)
428 this._cssMedia = cssMedia;
429 this._minWidthExpression = minWidthExpression;
430 this._maxWidthExpression = maxWidthExpression;
431 if (maxWidthExpression && !minWidthExpression)
432 this._section = WebInspector.MediaQueryInspector.Section.Max;
433 else if (minWidthExpression && maxWidthExpression)
434 this._section = WebInspector.MediaQueryInspector.Section.MinMax;
436 this._section = WebInspector.MediaQueryInspector.Section.Min;
440 * @param {!WebInspector.CSSMedia} cssMedia
441 * @param {!Array.<!WebInspector.CSSMediaQueryExpression>} mediaQueryExpressions
442 * @return {?WebInspector.MediaQueryInspector.MediaQueryUIModel}
444 WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaExpressions = function(cssMedia, mediaQueryExpressions)
446 var maxWidthExpression = null;
447 var maxWidthPixels = Number.MAX_VALUE;
448 var minWidthExpression = null;
449 var minWidthPixels = Number.MIN_VALUE;
450 for (var i = 0; i < mediaQueryExpressions.length; ++i) {
451 var expression = mediaQueryExpressions[i];
452 var feature = expression.feature();
453 if (feature.indexOf("width") === -1)
455 var pixels = expression.computedLength();
456 if (feature.startsWith("max-") && pixels < maxWidthPixels) {
457 maxWidthExpression = expression;
458 maxWidthPixels = pixels;
459 } else if (feature.startsWith("min-") && pixels > minWidthPixels) {
460 minWidthExpression = expression;
461 minWidthPixels = pixels;
464 if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression))
467 return new WebInspector.MediaQueryInspector.MediaQueryUIModel(cssMedia, minWidthExpression, maxWidthExpression);
470 WebInspector.MediaQueryInspector.MediaQueryUIModel.prototype = {
472 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
475 equals: function(other)
477 return this.compareTo(other) === 0;
481 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
484 dimensionsEqual: function(other)
486 return this.section() === other.section()
487 && (!this.minWidthExpression() || (this.minWidthExpression().computedLength() === other.minWidthExpression().computedLength()))
488 && (!this.maxWidthExpression() || (this.maxWidthExpression().computedLength() === other.maxWidthExpression().computedLength()));
492 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
495 compareTo: function(other)
497 if (this.section() !== other.section())
498 return this.section() - other.section();
499 if (this.dimensionsEqual(other)) {
500 var myLocation = this.uiLocation();
501 var otherLocation = other.uiLocation();
502 if (!myLocation && !otherLocation)
503 return this.mediaText().compareTo(other.mediaText());
504 if (myLocation && !otherLocation)
506 if (!myLocation && otherLocation)
508 return myLocation.uiSourceCode.uri().compareTo(otherLocation.uiSourceCode.uri()) || myLocation.lineNumber - otherLocation.lineNumber || myLocation.columnNumber - otherLocation.columnNumber;
510 if (this.section() === WebInspector.MediaQueryInspector.Section.Max)
511 return this.maxWidthExpression().computedLength() - other.maxWidthExpression().computedLength();
512 if (this.section() === WebInspector.MediaQueryInspector.Section.Min)
513 return this.minWidthExpression().computedLength() - other.minWidthExpression().computedLength();
514 return this.minWidthExpression().computedLength() - other.minWidthExpression().computedLength() || this.maxWidthExpression().computedLength() - other.maxWidthExpression().computedLength();
518 * @return {!WebInspector.MediaQueryInspector.Section}
522 return this._section;
528 mediaText: function()
530 return this._cssMedia.text;
534 * @return {?WebInspector.UILocation}
536 uiLocation: function()
538 return WebInspector.cssWorkspaceBinding.rawLocationToUILocation(this._cssMedia.rawLocation());
542 * @return {?WebInspector.CSSMediaQueryExpression}
544 minWidthExpression: function()
546 return this._minWidthExpression;
550 * @return {?WebInspector.CSSMediaQueryExpression}
552 maxWidthExpression: function()
554 return this._maxWidthExpression;