• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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