Home | History | Annotate | Download | only in toolbox
      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