Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / toolbox / MediaQueryInspector.js
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.element.addEventListener("webkitAnimationEnd", this._onAnimationEnd.bind(this), false);
17     this._mediaThrottler = new WebInspector.Throttler(100);
18
19     this._translateZero = 0;
20     this._offset = 0;
21     this._scale = 1;
22
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);
26
27     WebInspector.targetManager.observeTargets(this);
28
29     WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._renderMediaQueries.bind(this), this);
30 }
31
32 /**
33  * @enum {number}
34  */
35 WebInspector.MediaQueryInspector.Section = {
36     Max: 0,
37     MinMax: 1,
38     Min: 2
39 }
40
41 WebInspector.MediaQueryInspector.Events = {
42     HeightUpdated: "HeightUpdated"
43 }
44
45 WebInspector.MediaQueryInspector.prototype = {
46     /**
47      * @param {!WebInspector.Target} target
48      */
49     targetAdded: function(target)
50     {
51         // FIXME: adapt this to multiple targets.
52         if (this._target)
53             return;
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);
59     },
60
61     /**
62      * @param {!WebInspector.Target} target
63      */
64     targetRemoved: function(target)
65     {
66         if (target !== this._target)
67             return;
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);
72     },
73
74     /**
75      * @return {!Element}
76      */
77     rulerDecorationLayer: function()
78     {
79         return this._rulerDecorationLayer;
80     },
81
82     /**
83      * @return {!Array.<number>}
84      */
85     _mediaQueryThresholds: function()
86     {
87         if (!this._cachedQueryModels)
88             return [];
89         var thresholds = [];
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());
96         }
97         thresholds.sortNumbers();
98         return thresholds;
99     },
100
101     /**
102      * @param {!Event} event
103      */
104     _onRulerDecorationClicked: function(event)
105     {
106         var thresholdElement = event.target.enclosingNodeOrSelfWithClass("media-inspector-threshold-serif");
107         if (!thresholdElement)
108             return;
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");
120                 return;
121             }
122         }
123     },
124
125     /**
126      * @param {!Event} event
127      */
128     _onAnimationEnd: function(event)
129     {
130         event.target.classList.remove("media-inspector-marker-highlight-1");
131         event.target.classList.remove("media-inspector-marker-highlight-2");
132     },
133
134     /**
135      * @param {number} translate
136      * @param {number} offset
137      * @param {number} scale
138      */
139     setAxisTransform: function(translate, offset, scale)
140     {
141         if (this._translateZero === translate && this._offset === offset && Math.abs(this._scale - scale) < 1e-8)
142             return;
143         this._translateZero = translate;
144         this._offset = offset;
145         this._scale = scale;
146         this._renderMediaQueries();
147     },
148
149     /**
150      * @param {boolean} enabled
151      */
152     setEnabled: function(enabled)
153     {
154         this._enabled = enabled;
155         this._scheduleMediaQueriesUpdate();
156     },
157
158     /**
159      * @param {!Event} event
160      */
161     _onMediaQueryClicked: function(event)
162     {
163         var mediaQueryMarkerContainer = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker-container");
164         if (!mediaQueryMarkerContainer)
165             return;
166
167         /**
168          * @param {number} width
169          */
170         function setWidth(width)
171         {
172             WebInspector.overridesSupport.settings.deviceWidth.set(width);
173             WebInspector.overridesSupport.settings.emulateResolution.set(true);
174         }
175
176         var model = mediaQueryMarkerContainer._model;
177         if (model.section() === WebInspector.MediaQueryInspector.Section.Max) {
178             setWidth(model.maxWidthExpression().computedLength());
179             return;
180         }
181         if (model.section() === WebInspector.MediaQueryInspector.Section.Min) {
182             setWidth(model.minWidthExpression().computedLength());
183             return;
184         }
185         var currentWidth = WebInspector.overridesSupport.settings.deviceWidth.get();
186         if (currentWidth !== model.minWidthExpression().computedLength())
187             setWidth(model.minWidthExpression().computedLength());
188         else
189             setWidth(model.maxWidthExpression().computedLength());
190     },
191
192     /**
193      * @param {!Event} event
194      */
195     _onContextMenu: function(event)
196     {
197         var mediaQueryMarkerContainer = event.target.enclosingNodeOrSelfWithClass("media-inspector-marker-container");
198         if (!mediaQueryMarkerContainer)
199             return;
200
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));
208         }
209         contextMenu.show();
210     },
211
212     /**
213      * @param {!WebInspector.UILocation} location
214      */
215     _revealSourceLocation: function(location)
216     {
217         WebInspector.Revealer.reveal(location);
218     },
219
220     _scheduleMediaQueriesUpdate: function()
221     {
222         if (!this._enabled)
223             return;
224         this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this));
225     },
226
227     /**
228      * @param {!WebInspector.Throttler.FinishCallback} finishCallback
229      */
230     _refetchMediaQueries: function(finishCallback)
231     {
232         if (!this._enabled) {
233             finishCallback();
234             return;
235         }
236
237         /**
238          * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
239          * @this {!WebInspector.MediaQueryInspector}
240          */
241         function callback(cssMedias)
242         {
243             this._rebuildMediaQueries(cssMedias);
244             finishCallback();
245         }
246         this._target.cssModel.getMediaQueries(callback.bind(this));
247     },
248
249     /**
250      * @param {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>} models
251      * @return {!Array.<!WebInspector.MediaQueryInspector.MediaQueryUIModel>}
252      */
253     _squashAdjacentEqual: function(models)
254     {
255         var filtered = [];
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]);
260         }
261         return filtered;
262     },
263
264     /**
265      * @param {!Array.<!WebInspector.CSSMedia>} cssMedias
266      */
267     _rebuildMediaQueries: function(cssMedias)
268     {
269         var queryModels = [];
270         for (var i = 0; i < cssMedias.length; ++i) {
271             var cssMedia = cssMedias[i];
272             if (!cssMedia.mediaList)
273                 continue;
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);
277                 if (queryModel)
278                     queryModels.push(queryModel);
279             }
280         }
281         queryModels.sort(compareModels);
282         queryModels = this._squashAdjacentEqual(queryModels);
283
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]);
287         if (allEqual)
288             return;
289         this._cachedQueryModels = queryModels;
290         this._renderMediaQueries();
291
292         /**
293          * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model1
294          * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model2
295          * @return {number}
296          */
297         function compareModels(model1, model2)
298         {
299             return model1.compareTo(model2);
300         }
301     },
302
303     _renderMediaQueries: function()
304     {
305         if (!this._cachedQueryModels)
306             return;
307         this._renderRulerDecorations();
308         if (!this.isShowing())
309             return;
310
311         var markers = [];
312         var lastMarker = null;
313         for (var i = 0; i < this._cachedQueryModels.length; ++i) {
314             var model = this._cachedQueryModels[i];
315             if (!model.uiLocation())
316                 continue;
317             if (lastMarker && lastMarker.model.dimensionsEqual(model)) {
318                 lastMarker.locations.push(model.uiLocation());
319             } else {
320                 lastMarker = {
321                     model: model,
322                     locations: [ model.uiLocation() ]
323                 };
324                 markers.push(lastMarker);
325             }
326         }
327         var heightChanges = this.element.children.length !== markers.length;
328
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);
337         }
338         this.element.scrollTop = scrollTop;
339         this.element.classList.toggle("media-inspector-view-empty", !this.element.children.length);
340         if (heightChanges)
341             this.dispatchEventToListeners(WebInspector.MediaQueryInspector.Events.HeightUpdated);
342     },
343
344     /**
345      * @return {number}
346      */
347     _zoomFactor: function()
348     {
349         return WebInspector.zoomManager.zoomFactor() / this._scale;
350     },
351
352     _renderRulerDecorations: function()
353     {
354         this._rulerDecorationLayer.removeChildren();
355         var zoomFactor = this._zoomFactor();
356
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";
362         }
363     },
364
365     wasShown: function()
366     {
367         this._renderMediaQueries();
368     },
369
370     /**
371      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} model
372      * @return {!Element}
373      */
374     _createElementFromMediaQueryModel: function(model)
375     {
376         var zoomFactor = this._zoomFactor();
377         var minWidthValue = model.minWidthExpression() ? model.minWidthExpression().computedLength() : 0;
378
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"
383         ];
384         var container = document.createElementWithClass("div", "media-inspector-marker-container hbox");
385         container.classList.add(styleClassPerSection[model.section()]);
386
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;
395         else
396             markerElement.style.right = "0";
397         if (typeof widthPixelValue === "number")
398             markerElement.style.width = widthPixelValue + "px";
399
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";
405         }
406
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";
412         }
413
414         return container;
415     },
416
417     __proto__: WebInspector.View.prototype
418 };
419
420 /**
421  * @constructor
422  * @param {!WebInspector.CSSMedia} cssMedia
423  * @param {?WebInspector.CSSMediaQueryExpression} minWidthExpression
424  * @param {?WebInspector.CSSMediaQueryExpression} maxWidthExpression
425  */
426 WebInspector.MediaQueryInspector.MediaQueryUIModel = function(cssMedia, minWidthExpression, maxWidthExpression)
427 {
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;
435     else
436         this._section = WebInspector.MediaQueryInspector.Section.Min;
437 }
438
439 /**
440  * @param {!WebInspector.CSSMedia} cssMedia
441  * @param {!Array.<!WebInspector.CSSMediaQueryExpression>} mediaQueryExpressions
442  * @return {?WebInspector.MediaQueryInspector.MediaQueryUIModel}
443  */
444 WebInspector.MediaQueryInspector.MediaQueryUIModel.createFromMediaExpressions = function(cssMedia, mediaQueryExpressions)
445 {
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)
454             continue;
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;
462         }
463     }
464     if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression))
465         return null;
466
467     return new WebInspector.MediaQueryInspector.MediaQueryUIModel(cssMedia, minWidthExpression, maxWidthExpression);
468 }
469
470 WebInspector.MediaQueryInspector.MediaQueryUIModel.prototype = {
471     /**
472      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
473      * @return {boolean}
474      */
475     equals: function(other)
476     {
477         return this.compareTo(other) === 0;
478     },
479
480     /**
481      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
482      * @return {boolean}
483      */
484     dimensionsEqual: function(other)
485     {
486         return this.section() === other.section()
487             && (!this.minWidthExpression() || (this.minWidthExpression().computedLength() === other.minWidthExpression().computedLength()))
488             && (!this.maxWidthExpression() || (this.maxWidthExpression().computedLength() === other.maxWidthExpression().computedLength()));
489     },
490
491     /**
492      * @param {!WebInspector.MediaQueryInspector.MediaQueryUIModel} other
493      * @return {number}
494      */
495     compareTo: function(other)
496     {
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)
505                 return 1;
506             if (!myLocation && otherLocation)
507                 return -1;
508             return myLocation.uiSourceCode.uri().compareTo(otherLocation.uiSourceCode.uri()) || myLocation.lineNumber - otherLocation.lineNumber || myLocation.columnNumber - otherLocation.columnNumber;
509         }
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();
515     },
516
517     /**
518      * @return {!WebInspector.MediaQueryInspector.Section}
519      */
520     section: function()
521     {
522         return this._section;
523     },
524
525     /**
526      * @return {string}
527      */
528     mediaText: function()
529     {
530         return this._cssMedia.text;
531     },
532
533     /**
534      * @return {?WebInspector.UILocation}
535      */
536     uiLocation: function()
537     {
538         return WebInspector.cssWorkspaceBinding.rawLocationToUILocation(this._cssMedia.rawLocation());
539     },
540
541     /**
542      * @return {?WebInspector.CSSMediaQueryExpression}
543      */
544     minWidthExpression: function()
545     {
546         return this._minWidthExpression;
547     },
548
549     /**
550      * @return {?WebInspector.CSSMediaQueryExpression}
551      */
552     maxWidthExpression: function()
553     {
554         return this._maxWidthExpression;
555     }
556 }