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 * @implements {WebInspector.TargetManager.Observer} 9 */ 10 WebInspector.MediaQueryInspector = function() 11 { 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); 17 18 this._offset = 0; 19 this._scale = 1; 20 this._lastReportedCount = 0; 21 22 WebInspector.targetManager.observeTargets(this); 23 24 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._renderMediaQueries.bind(this), this); 25 } 26 27 /** 28 * @enum {number} 29 */ 30 WebInspector.MediaQueryInspector.Section = { 31 Max: 0, 32 MinMax: 1, 33 Min: 2 34 } 35 36 WebInspector.MediaQueryInspector.Events = { 37 HeightUpdated: "HeightUpdated", 38 CountUpdated: "CountUpdated" 39 } 40 41 WebInspector.MediaQueryInspector.prototype = { 42 /** 43 * @param {!WebInspector.Target} target 44 */ 45 targetAdded: function(target) 46 { 47 // FIXME: adapt this to multiple targets. 48 if (this._target) 49 return; 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); 55 }, 56 57 /** 58 * @param {!WebInspector.Target} target 59 */ 60 targetRemoved: function(target) 61 { 62 if (target !== this._target) 63 return; 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); 68 }, 69 70 /** 71 * @param {number} offset 72 * @param {number} scale 73 */ 74 setAxisTransform: function(offset, scale) 75 { 76 if (this._offset === offset && Math.abs(this._scale - scale) < 1e-8) 77 return; 78 this._offset = offset; 79 this._scale = scale; 80 this._renderMediaQueries(); 81 }, 82 83 /** 84 * @param {boolean} enabled 85 */ 86 setEnabled: function(enabled) 87 { 88 this._enabled = enabled; 89 this._scheduleMediaQueriesUpdate(); 90 }, 91 92 /** 93 * @param {!Event} event 94 */ 95 _onMediaQueryClicked: function(event) 96 { 97 var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker"); 98 if (!mediaQueryMarker) 99 return; 100 101 /** 102 * @param {number} width 103 */ 104 function setWidth(width) 105 { 106 WebInspector.overridesSupport.settings.deviceWidth.set(width); 107 WebInspector.overridesSupport.settings.emulateResolution.set(true); 108 } 109 110 var model = mediaQueryMarker._model; 111 if (model.section() === WebInspector.MediaQueryInspector.Section.Max) { 112 setWidth(model.maxWidthExpression().computedLength()); 113 return; 114 } 115 if (model.section() === WebInspector.MediaQueryInspector.Section.Min) { 116 setWidth(model.minWidthExpression().computedLength()); 117 return; 118 } 119 var currentWidth = WebInspector.overridesSupport.settings.deviceWidth.get(); 120 if (currentWidth !== model.minWidthExpression().computedLength()) 121 setWidth(model.minWidthExpression().computedLength()); 122 else 123 setWidth(model.maxWidthExpression().computedLength()); 124 }, 125 126 /** 127 * @param {!Event} event 128 */ 129 _onContextMenu: function(event) 130 { 131 var mediaQueryMarker = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker"); 132 if (!mediaQueryMarker) 133 return; 134 135 var locations = mediaQueryMarker._locations; 136 var uiLocations = new StringMap(); 137 for (var i = 0; i < locations.length; ++i) { 138 var uiLocation = WebInspector.cssWorkspaceBinding.rawLocationToUILocation(locations[i]); 139 if (!uiLocation) 140 continue; 141 var descriptor = String.sprintf("%s:%d:%d", uiLocation.uiSourceCode.uri(), uiLocation.lineNumber + 1, uiLocation.columnNumber + 1); 142 uiLocations.set(descriptor, uiLocation); 143 } 144 145 var contextMenuItems = uiLocations.keys().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)))); 151 } 152 contextMenu.show(); 153 }, 154 155 /** 156 * @param {!WebInspector.UILocation} location 157 */ 158 _revealSourceLocation: function(location) 159 { 160 WebInspector.Revealer.reveal(location); 161 }, 162 163 _scheduleMediaQueriesUpdate: function() 164 { 165 if (!this._enabled) 166 return; 167 this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this)); 168 }, 169 170 /** 171 * @param {!WebInspector.Throttler.FinishCallback} finishCallback 172 */ 173 _refetchMediaQueries: function(finishCallback) 174 { 175 if (!this._enabled) { 176 finishCallback(); 177 return; 178 } 179 180 /** 181 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias 182 * @this {!WebInspector.MediaQueryInspector} 183 */ 184 function callback(cssMedias) 185 { 186 this._rebuildMediaQueries(cssMedias); 187 finishCallback(); 188 } 189 this._target.cssModel.getMediaQueries(callback.bind(this)); 190 }, 191 192 /** 193 * @param {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>} models 194 * @return {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>} 195 */ 196 _squashAdjacentEqual: function(models) 197 { 198 var filtered = []; 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]); 203 } 204 return filtered; 205 }, 206 207 /** 208 * @param {!Array.<!WebInspector.CSSMedia>} cssMedias 209 */ 210 _rebuildMediaQueries: function(cssMedias) 211 { 212 var queryModels = []; 213 for (var i = 0; i < cssMedias.length; ++i) { 214 var cssMedia = cssMedias[i]; 215 if (!cssMedia.mediaList) 216 continue; 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); 222 } 223 } 224 queryModels.sort(compareModels); 225 queryModels = this._squashAdjacentEqual(queryModels); 226 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]); 230 if (allEqual) 231 return; 232 this._cachedQueryModels = queryModels; 233 this._renderMediaQueries(); 234 235 /** 236 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model1 237 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model2 238 * @return {number} 239 */ 240 function compareModels(model1, model2) 241 { 242 return model1.compareTo(model2); 243 } 244 }, 245 246 _renderMediaQueries: function() 247 { 248 if (!this._cachedQueryModels) 249 return; 250 251 var markers = []; 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(); 258 } else { 259 lastMarker = { 260 active: model.active(), 261 model: model, 262 locations: [ model.rawLocation() ] 263 }; 264 markers.push(lastMarker); 265 } 266 } 267 268 if (markers.length !== this._lastReportedCount) { 269 this._lastReportedCount = markers.length; 270 this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.CountUpdated, markers.length); 271 } 272 273 if (!this.isShowing()) 274 return; 275 276 var oldChildrenCount = this.element.children.length; 277 var scrollTop = this.element.scrollTop; 278 this.element.removeChildren(); 279 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); 290 } 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); 295 }, 296 297 /** 298 * @return {number} 299 */ 300 _zoomFactor: function() 301 { 302 return WebInspector.zoomManager.zoomFactor() / this._scale; 303 }, 304 305 wasShown: function() 306 { 307 this._renderMediaQueries(); 308 }, 309 310 /** 311 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model 312 * @return {!Element} 313 */ 314 _createElementFromMediaQueryModel: function(model) 315 { 316 var zoomFactor = this._zoomFactor(); 317 var minWidthValue = model.minWidthExpression() ? model.minWidthExpression().computedLength() : 0; 318 319 const styleClassPerSection = [ 320 "media-inspector-marker-max-width", 321 "media-inspector-marker-min-max-width", 322 "media-inspector-marker-min-width" 323 ]; 324 var markerElement = document.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; 333 else 334 markerElement.style.right = "0"; 335 if (typeof widthPixelValue === "number") 336 markerElement.style.width = widthPixelValue + "px"; 337 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(); 342 } 343 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(); 348 } 349 markerElement.title = model.mediaText(); 350 351 return markerElement; 352 }, 353 354 __proto__: WebInspector.View.prototype 355 }; 356 357 /** 358 * @constructor 359 * @param {!WebInspector.CSSMedia} cssMedia 360 * @param {?WebInspector.CSSMediaQueryExpression} minWidthExpression 361 * @param {?WebInspector.CSSMediaQueryExpression} maxWidthExpression 362 * @param {boolean} active 363 */ 364 WebInspector.MediaQueryInspector.MediaQueryUIModel = function(cssMedia, minWidthExpression, maxWidthExpression, active) 365 { 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; 374 else 375 this._section = WebInspector.MediaQueryInspector.Section.Min; 376 } 377 378 /** 379 * @param {!WebInspector.CSSMedia} cssMedia 380 * @param {!WebInspector.CSSMediaQuery} mediaQuery 381 * @return {?WebInspector.MediaQueryInspector.MediaQueryUIModel} 382 */ 383 WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaQuery = function(cssMedia, mediaQuery) 384 { 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) 394 continue; 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; 402 } 403 } 404 if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression)) 405 return null; 406 407 return new WebInspector.MediaQueryInspector.MediaQueryUIModel(cssMedia, minWidthExpression, maxWidthExpression, mediaQuery.active()); 408 } 409 410 WebInspector.MediaQueryInspector.MediaQueryUIModel.prototype = { 411 /** 412 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other 413 * @return {boolean} 414 */ 415 equals: function(other) 416 { 417 return this.compareTo(other) === 0; 418 }, 419 420 /** 421 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other 422 * @return {boolean} 423 */ 424 dimensionsEqual: function(other) 425 { 426 return this.section() === other.section() 427 && (!this.minWidthExpression() || (this.minWidthExpression().computedLength() === other.minWidthExpression().computedLength())) 428 && (!this.maxWidthExpression() || (this.maxWidthExpression().computedLength() === other.maxWidthExpression().computedLength())); 429 }, 430 431 /** 432 * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other 433 * @return {number} 434 */ 435 compareTo: function(other) 436 { 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) 445 return 1; 446 if (!myLocation && otherLocation) 447 return -1; 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; 451 } 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(); 457 }, 458 459 /** 460 * @return {!WebInspector.MediaQueryInspector.Section} 461 */ 462 section: function() 463 { 464 return this._section; 465 }, 466 467 /** 468 * @return {string} 469 */ 470 mediaText: function() 471 { 472 return this._cssMedia.text; 473 }, 474 475 /** 476 * @return {?WebInspector.CSSLocation} 477 */ 478 rawLocation: function() 479 { 480 if (!this._rawLocation) 481 this._rawLocation = this._cssMedia.rawLocation(); 482 return this._rawLocation; 483 }, 484 485 /** 486 * @return {?WebInspector.CSSMediaQueryExpression} 487 */ 488 minWidthExpression: function() 489 { 490 return this._minWidthExpression; 491 }, 492 493 /** 494 * @return {?WebInspector.CSSMediaQueryExpression} 495 */ 496 maxWidthExpression: function() 497 { 498 return this._maxWidthExpression; 499 }, 500 501 /** 502 * @return {boolean} 503 */ 504 active: function() 505 { 506 return this._active; 507 } 508 } 509