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. 4 5/** 6 * @constructor 7 * @extends {WebInspector.View} 8 */ 9WebInspector.MediaQueryInspector = function() 10{ 11 WebInspector.View.call(this); 12 this.element.classList.add("media-inspector-view"); 13 this.element.addEventListener("click", this._onMediaQueryClicked.bind(this), false); 14 this.element.addEventListener("contextmenu", this._onContextMenu.bind(this), false); 15 this._mediaThrottler = new WebInspector.Throttler(100); 16 this._zeroOffset = 0; 17 18 this._rulerDecorationLayer = document.createElementWithClass("div", "fill"); 19 this._rulerDecorationLayer.classList.add("media-inspector-ruler-decoration"); 20 this._rulerDecorationLayer.addEventListener("click", this._onRulerDecorationClicked.bind(this), false); 21 22 WebInspector.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this); 23 WebInspector.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this); 24 WebInspector.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this); 25 WebInspector.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this); 26 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._renderMediaQueries.bind(this), this); 27 this._scheduleMediaQueriesUpdate(); 28} 29 30WebInspector.MediaQueryInspector.Events = { 31 HeightUpdated: "HeightUpdated" 32} 33 34WebInspector.MediaQueryInspector._ThresholdPadding = 1; 35 36WebInspector.MediaQueryInspector.prototype = { 37 /** 38 * @return {!Element} 39 */ 40 rulerDecorationLayer: function() 41 { 42 return this._rulerDecorationLayer; 43 }, 44 45 /** 46 * @return {!Array.<number>} 47 */ 48 _mediaQueryThresholds: function() 49 { 50 if (!this._cachedQueryModels) 51 return []; 52 var thresholds = []; 53 for (var i = 0; i < this._cachedQueryModels.length; ++i) { 54 var model = this._cachedQueryModels[i]; 55 if (model.minWidthExpression) 56 thresholds.push(model.minWidthExpression.computedLength()); 57 if (model.maxWidthExpression) 58 thresholds.push(model.maxWidthExpression.computedLength()); 59 } 60 thresholds.sortNumbers(); 61 var filtered = []; 62 for (var i = 0; i < thresholds.length; ++i) { 63 if (i == 0 || thresholds[i] - filtered.peekLast() > WebInspector.MediaQueryInspector._ThresholdPadding) 64 filtered.push(thresholds[i]); 65 } 66 return filtered; 67 }, 68 69 /** 70 * @param {?Event} event 71 */ 72 _onRulerDecorationClicked: function(event) 73 { 74 var thresholdElement = event.target.enclosingNodeOrSelfWithClass("media-inspector-threshold-serif"); 75 if (!thresholdElement) 76 return; 77 WebInspector.settings.showMediaQueryInspector.set(true); 78 var revealValue = thresholdElement._value; 79 for (var mediaQueryContainer = this.element.firstChild; mediaQueryContainer; mediaQueryContainer = mediaQueryContainer.nextSibling) { 80 var model = mediaQueryContainer._model; 81 if ((model.minWidthExpression && Math.abs(model.minWidthExpression.computedLength() - revealValue) <= WebInspector.MediaQueryInspector._ThresholdPadding) 82 || (model.maxWidthExpression && Math.abs(model.maxWidthExpression.computedLength() - revealValue) <= WebInspector.MediaQueryInspector._ThresholdPadding)) { 83 mediaQueryContainer.scrollIntoViewIfNeeded(false); 84 return; 85 } 86 } 87 }, 88 89 /** 90 * @param {number} offset 91 */ 92 translateZero: function(offset) 93 { 94 this._zeroOffset = offset; 95 this._renderMediaQueries(); 96 }, 97 98 /** 99 * @param {boolean} enabled 100 */ 101 setEnabled: function(enabled) 102 { 103 this._enabled = enabled; 104 }, 105 106 /** 107 * @param {?Event} event 108 */ 109 _onMediaQueryClicked: function(event) 110 { 111 var mediaQueryMarkerContainer = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker-container"); 112 if (!mediaQueryMarkerContainer) 113 return; 114 var model = mediaQueryMarkerContainer._model; 115 if (model.sectionNumber === 0) { 116 WebInspector.overridesSupport.settings.deviceWidth.set(model.maxWidthExpression.computedLength()); 117 return; 118 } 119 if (model.sectionNumber === 2) { 120 WebInspector.overridesSupport.settings.deviceWidth.set(model.minWidthExpression.computedLength()); 121 return; 122 } 123 var currentWidth = WebInspector.overridesSupport.settings.deviceWidth.get(); 124 if (currentWidth !== model.minWidthExpression.computedLength()) 125 WebInspector.overridesSupport.settings.deviceWidth.set(model.minWidthExpression.computedLength()); 126 else 127 WebInspector.overridesSupport.settings.deviceWidth.set(model.maxWidthExpression.computedLength()); 128 }, 129 130 /** 131 * @param {?Event} event 132 */ 133 _onContextMenu: function(event) 134 { 135 var mediaQueryMarkerContainer = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker-container"); 136 if (!mediaQueryMarkerContainer) 137 return; 138 139 var model = mediaQueryMarkerContainer._model; 140 var uiSourceCode = WebInspector.workspace.uiSourceCodeForURL(model.sourceURL); 141 if (!uiSourceCode || typeof model.lineNumber !== "number" || typeof model.columnNumber !== "number") 142 return; 143 144 var contextMenu = new WebInspector.ContextMenu(event); 145 var location = uiSourceCode.uiLocation(model.lineNumber, model.columnNumber); 146 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in source code" : "Reveal In Source Code"), this._revealSourceLocation.bind(this, location)); 147 contextMenu.show(); 148 }, 149 150 /** 151 * @param {!WebInspector.UILocation} location 152 */ 153 _revealSourceLocation: function(location) 154 { 155 WebInspector.Revealer.reveal(location); 156 }, 157 158 _scheduleMediaQueriesUpdate: function() 159 { 160 if (!this._enabled) 161 return; 162 this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this)); 163 }, 164 165 /** 166 * @param {!WebInspector.Throttler.FinishCallback} finishCallback 167 */ 168 _refetchMediaQueries: function(finishCallback) 169 { 170 if (!this._enabled) { 171 finishCallback(); 172 return; 173 } 174 175 /** 176 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias 177 * @this {!WebInspector.MediaQueryInspector} 178 */ 179 function callback(cssMedias) 180 { 181 this._rebuildMediaQueries(cssMedias); 182 finishCallback(); 183 } 184 WebInspector.cssModel.getMediaQueries(callback.bind(this)); 185 }, 186 187 /** 188 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias 189 */ 190 _rebuildMediaQueries: function(cssMedias) 191 { 192 var queryModels = []; 193 for (var i = 0; i < cssMedias.length; ++i) { 194 var cssMedia = cssMedias[i]; 195 if (!cssMedia.mediaList) 196 continue; 197 for (var j = 0; j < cssMedia.mediaList.length; ++j) { 198 var mediaQueryExpressions = cssMedia.mediaList[j]; 199 var queryModel = this._mediaQueryToUIModel(cssMedia, mediaQueryExpressions); 200 if (queryModel) 201 queryModels.push(queryModel); 202 } 203 } 204 queryModels.sort(queryModelComparator); 205 this._cachedQueryModels = queryModels; 206 this._renderMediaQueries(); 207 208 /** 209 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model1 210 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model2 211 * @return {number} 212 */ 213 function queryModelComparator(model1, model2) 214 { 215 if (model1.sectionNumber !== model2.sectionNumber) 216 return model1.sectionNumber - model2.sectionNumber; 217 if (model1.sectionNumber === 0) 218 return model1.maxWidthExpression.computedLength() - model2.maxWidthExpression.computedLength(); 219 if (model1.sectionNumber === 2) 220 return model1.minWidthExpression.computedLength() - model2.minWidthExpression.computedLength(); 221 return model1.minWidthExpression.computedLength() - model2.minWidthExpression.computedLength() || model1.maxWidthExpression.computedLength() - model2.maxWidthExpression.computedLength(); 222 } 223 }, 224 225 _renderMediaQueries: function() 226 { 227 if (!this._cachedQueryModels) 228 return; 229 this._renderRulerDecorations(); 230 if (!this.isShowing()) 231 return; 232 var scrollTop = this.element.scrollTop; 233 var heightChanges = this.element.children.length !== this._cachedQueryModels.length; 234 this.element.removeChildren(); 235 for (var i = 0; i < this._cachedQueryModels.length; ++i) { 236 var model = this._cachedQueryModels[i]; 237 var bar = this._createElementFromMediaQueryModel(model); 238 bar._model = model; 239 this.element.appendChild(bar); 240 } 241 this.element.scrollTop = scrollTop; 242 this.element.classList.toggle("media-inspector-view-fixed-height", this._cachedQueryModels.length > 5); 243 if (heightChanges) 244 this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.HeightUpdated); 245 }, 246 247 _renderRulerDecorations: function() 248 { 249 this._rulerDecorationLayer.removeChildren(); 250 var zoomFactor = WebInspector.zoomManager.zoomFactor(); 251 252 var thresholds = this._mediaQueryThresholds(); 253 for (var i = 0; i < thresholds.length; ++i) { 254 var thresholdElement = this._rulerDecorationLayer.createChild("div", "media-inspector-threshold-serif"); 255 thresholdElement._value = thresholds[i]; 256 thresholdElement.style.left = thresholds[i] / zoomFactor + "px"; 257 } 258 }, 259 260 wasShown: function() 261 { 262 this._renderMediaQueries(); 263 }, 264 265 /** 266 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model 267 * @return {!Element} 268 */ 269 _createElementFromMediaQueryModel: function(model) 270 { 271 var zoomFactor = WebInspector.zoomManager.zoomFactor(); 272 var minWidthValue = model.minWidthExpression ? model.minWidthExpression.computedLength() : 0; 273 274 var container = document.createElementWithClass("div", "media-inspector-marker-container"); 275 // Enforce container height if it does not have any children in normal flow. 276 if (model.sectionNumber === 0) 277 container.textContent = "."; 278 var markerElement = container.createChild("div", "media-inspector-marker"); 279 const styleClassPerSection = [ 280 "media-inspector-marker-max-width", 281 "media-inspector-marker-min-max-width", 282 "media-inspector-marker-min-width" 283 ]; 284 markerElement.classList.add(styleClassPerSection[model.sectionNumber]); 285 markerElement.style.left = (minWidthValue ? minWidthValue / zoomFactor + this._zeroOffset : 0) + "px"; 286 if (model.maxWidthExpression && model.minWidthExpression) 287 markerElement.style.width = (model.maxWidthExpression.computedLength() - minWidthValue) / zoomFactor + "px"; 288 else if (model.maxWidthExpression) 289 markerElement.style.width = model.maxWidthExpression.computedLength() / zoomFactor + this._zeroOffset + "px"; 290 else 291 markerElement.style.right = "0"; 292 293 const minLabelOverlapMarkerValue = 4; 294 if (model.minWidthExpression) { 295 var minLabelContainer = container.createChild("div", "media-inspector-min-width-label-container"); 296 minLabelContainer.style.maxWidth = markerElement.style.left; 297 minLabelContainer.textContent = "."; 298 var label = document.createElementWithClass("span", "media-inspector-marker-label"); 299 label.classList.add("media-inspector-min-label"); 300 label.textContent = model.minWidthExpression.computedLength() + "px"; 301 label.style.right = -minLabelOverlapMarkerValue + "px"; 302 minLabelContainer.appendChild(label); 303 } 304 // Image might not be loaded on the moment of label measuring. To avoid 305 // incorrect rendering, image size is hardcoded and label is measured without image. 306 const arrowImageWidth = 13; 307 const maxLabelOverlapMarkerValue = 2 / zoomFactor; 308 if (model.maxWidthExpression) { 309 var label = document.createElementWithClass("span", "media-inspector-marker-label"); 310 label.textContent = model.maxWidthExpression.computedLength() + "px"; 311 var labelSize = label.measurePreferredSize(this.element); 312 // Append arrow image to label. 313 label.classList.add("media-inspector-max-label"); 314 label.style.right = -labelSize.width - arrowImageWidth - maxLabelOverlapMarkerValue + "px"; 315 markerElement.appendChild(label); 316 } 317 return container; 318 }, 319 320 /** 321 * @param {!WebInspector.CSSMedia} parentCSSMedia 322 * @param {!Array.<!WebInspector.CSSMediaQueryExpression>} mediaQueryExpressions 323 * @return {?WebInspector.MediaQueryInspector.MediaQueryUIModel} 324 */ 325 _mediaQueryToUIModel: function(parentCSSMedia, mediaQueryExpressions) 326 { 327 var maxWidthExpression = null; 328 var maxWidthPixels = Number.MAX_VALUE; 329 var minWidthExpression = null; 330 var minWidthPixels = Number.MIN_VALUE; 331 for (var i = 0; i < mediaQueryExpressions.length; ++i) { 332 var expression = mediaQueryExpressions[i]; 333 var feature = expression.feature(); 334 if (feature.indexOf("width") === -1) 335 continue; 336 var pixels = expression.computedLength(); 337 if (feature.startsWith("max-") && pixels < maxWidthPixels) { 338 maxWidthExpression = expression; 339 maxWidthPixels = pixels; 340 } else if (feature.startsWith("min-") && pixels > minWidthPixels) { 341 minWidthExpression = expression; 342 minWidthPixels = pixels; 343 } 344 } 345 if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression)) 346 return null; 347 348 var sectionNumber; 349 if (maxWidthExpression && !minWidthExpression) 350 sectionNumber = 0; 351 else if (minWidthExpression && maxWidthExpression) 352 sectionNumber = 1; 353 else 354 sectionNumber = 2; 355 356 return { 357 sectionNumber: sectionNumber, 358 mediaText: parentCSSMedia.text, 359 sourceURL: parentCSSMedia.sourceURL, 360 lineNumber: parentCSSMedia.lineNumberInSource(), 361 columnNumber: parentCSSMedia.columnNumberInSource(), 362 minWidthExpression: minWidthExpression, 363 maxWidthExpression: maxWidthExpression 364 }; 365 }, 366 367 __proto__: WebInspector.View.prototype 368}; 369 370/** @typedef {{sectionNumber: number, mediaText: string, sourceURL: string, lineNumber: (?number|undefined), columnNumber: (?number|undefined), minWidthExpression: ?WebInspector.CSSMediaQueryExpression, maxWidthExpression: ?WebInspector.CSSMediaQueryExpression}} */ 371WebInspector.MediaQueryInspector.MediaQueryUIModel; 372