UNPKG

chrome-devtools-frontend

Version:
567 lines (510 loc) • 19.9 kB
// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Bindings from '../bindings/bindings.js'; import * as Common from '../common/common.js'; import * as Platform from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; import * as Workspace from '../workspace/workspace.js'; // eslint-disable-line no-unused-vars /** * @implements {SDK.SDKModel.SDKModelObserver<!SDK.CSSModel.CSSModel>} */ export class MediaQueryInspector extends UI.Widget.Widget { /** * @param {function():number} getWidthCallback * @param {function(number):void} setWidthCallback */ constructor(getWidthCallback, setWidthCallback) { super(true); this.registerRequiredCSS('emulation/mediaQueryInspector.css', {enableLegacyPatching: true}); this.contentElement.classList.add('media-inspector-view'); this.contentElement.addEventListener('click', this._onMediaQueryClicked.bind(this), false); this.contentElement.addEventListener('contextmenu', this._onContextMenu.bind(this), false); this._mediaThrottler = new Common.Throttler.Throttler(0); this._getWidthCallback = getWidthCallback; this._setWidthCallback = setWidthCallback; this._scale = 1; /** @type {!WeakMap<!Element, !MediaQueryUIModel>} */ this.elementsToMediaQueryModel = new WeakMap(); /** @type {!WeakMap<!Element, !Array<!SDK.CSSModel.CSSLocation>>} */ this.elementsToCSSLocations = new WeakMap(); SDK.SDKModel.TargetManager.instance().observeModels(SDK.CSSModel.CSSModel, this); UI.ZoomManager.ZoomManager.instance().addEventListener( UI.ZoomManager.Events.ZoomChanged, this._renderMediaQueries.bind(this), this); } /** * @override * @param {!SDK.CSSModel.CSSModel} cssModel */ modelAdded(cssModel) { // FIXME: adapt this to multiple targets. if (this._cssModel) { return; } this._cssModel = cssModel; this._cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this); this._cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this); this._cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this); this._cssModel.addEventListener( SDK.CSSModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this); } /** * @override * @param {!SDK.CSSModel.CSSModel} cssModel */ modelRemoved(cssModel) { if (cssModel !== this._cssModel) { return; } this._cssModel.removeEventListener(SDK.CSSModel.Events.StyleSheetAdded, this._scheduleMediaQueriesUpdate, this); this._cssModel.removeEventListener(SDK.CSSModel.Events.StyleSheetRemoved, this._scheduleMediaQueriesUpdate, this); this._cssModel.removeEventListener(SDK.CSSModel.Events.StyleSheetChanged, this._scheduleMediaQueriesUpdate, this); this._cssModel.removeEventListener( SDK.CSSModel.Events.MediaQueryResultChanged, this._scheduleMediaQueriesUpdate, this); delete this._cssModel; } /** * @param {number} scale */ setAxisTransform(scale) { if (Math.abs(this._scale - scale) < 1e-8) { return; } this._scale = scale; this._renderMediaQueries(); } /** * @param {!Event} event */ _onMediaQueryClicked(event) { const mediaQueryMarker = /** @type {!Node} */ (event.target).enclosingNodeOrSelfWithClass('media-inspector-bar'); if (!mediaQueryMarker) { return; } const model = this.elementsToMediaQueryModel.get(mediaQueryMarker); if (!model) { return; } const modelMaxWidth = model.maxWidthExpression(); const modelMinWidth = model.minWidthExpression(); if (model.section() === Section.Max) { this._setWidthCallback(modelMaxWidth ? modelMaxWidth.computedLength() || 0 : 0); return; } if (model.section() === Section.Min) { this._setWidthCallback(modelMinWidth ? modelMinWidth.computedLength() || 0 : 0); return; } const currentWidth = this._getWidthCallback(); if (modelMinWidth && currentWidth !== modelMinWidth.computedLength()) { this._setWidthCallback(modelMinWidth.computedLength() || 0); } else { this._setWidthCallback(modelMaxWidth ? modelMaxWidth.computedLength() || 0 : 0); } } /** * @param {!Event} event */ _onContextMenu(event) { if (!this._cssModel || !this._cssModel.isEnabled()) { return; } const mediaQueryMarker = /** @type {!Node} */ (event.target).enclosingNodeOrSelfWithClass('media-inspector-bar'); if (!mediaQueryMarker) { return; } const locations = this.elementsToCSSLocations.get(mediaQueryMarker) || []; const uiLocations = new Map(); for (let i = 0; i < locations.length; ++i) { const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().rawLocationToUILocation(locations[i]); if (!uiLocation) { continue; } const descriptor = typeof uiLocation.columnNumber === 'number' ? Platform.StringUtilities.sprintf( '%s:%d:%d', uiLocation.uiSourceCode.url(), uiLocation.lineNumber + 1, uiLocation.columnNumber + 1) : Platform.StringUtilities.sprintf('%s:%d', uiLocation.uiSourceCode.url(), uiLocation.lineNumber + 1); uiLocations.set(descriptor, uiLocation); } const contextMenuItems = [...uiLocations.keys()].sort(); const contextMenu = new UI.ContextMenu.ContextMenu(event); const subMenuItem = contextMenu.defaultSection().appendSubMenuItem(Common.UIString.UIString('Reveal in source code')); for (let i = 0; i < contextMenuItems.length; ++i) { const title = contextMenuItems[i]; subMenuItem.defaultSection().appendItem( title, this._revealSourceLocation.bind( this, /** @type {!Workspace.UISourceCode.UILocation} */ (uiLocations.get(title)))); } contextMenu.show(); } /** * @param {!Workspace.UISourceCode.UILocation} location */ _revealSourceLocation(location) { Common.Revealer.reveal(location); } _scheduleMediaQueriesUpdate() { if (!this.isShowing()) { return; } this._mediaThrottler.schedule(this._refetchMediaQueries.bind(this)); } _refetchMediaQueries() { if (!this.isShowing() || !this._cssModel) { return Promise.resolve(); } return this._cssModel.mediaQueriesPromise().then(this._rebuildMediaQueries.bind(this)); } /** * @param {!Array.<!MediaQueryUIModel>} models * @return {!Array.<!MediaQueryUIModel>} */ _squashAdjacentEqual(models) { const filtered = []; for (let i = 0; i < models.length; ++i) { const last = filtered[filtered.length - 1]; if (!last || !last.equals(models[i])) { filtered.push(models[i]); } } return filtered; } /** * @param {!Array.<!SDK.CSSMedia.CSSMedia>} cssMedias */ _rebuildMediaQueries(cssMedias) { let queryModels = []; for (let i = 0; i < cssMedias.length; ++i) { const cssMedia = cssMedias[i]; if (!cssMedia.mediaList) { continue; } for (let j = 0; j < cssMedia.mediaList.length; ++j) { const mediaQuery = cssMedia.mediaList[j]; const queryModel = MediaQueryUIModel.createFromMediaQuery(cssMedia, mediaQuery); if (queryModel) { queryModels.push(queryModel); } } } queryModels.sort(compareModels); queryModels = this._squashAdjacentEqual(queryModels); let allEqual = this._cachedQueryModels && this._cachedQueryModels.length === queryModels.length; for (let i = 0; allEqual && i < queryModels.length; ++i) { allEqual = allEqual && this._cachedQueryModels && this._cachedQueryModels[i].equals(queryModels[i]); } if (allEqual) { return; } this._cachedQueryModels = queryModels; this._renderMediaQueries(); /** * @param {!MediaQueryUIModel} model1 * @param {!MediaQueryUIModel} model2 * @return {number} */ function compareModels(model1, model2) { return model1.compareTo(model2); } } _renderMediaQueries() { if (!this._cachedQueryModels || !this.isShowing()) { return; } const markers = []; let lastMarker = null; for (let i = 0; i < this._cachedQueryModels.length; ++i) { const model = this._cachedQueryModels[i]; if (lastMarker && lastMarker.model.dimensionsEqual(model)) { lastMarker.active = lastMarker.active || model.active(); } else { lastMarker = { active: model.active(), model, locations: /** @type {!Array<!SDK.CSSModel.CSSLocation>} */ ([]), }; markers.push(lastMarker); } const rawLocation = model.rawLocation(); if (rawLocation) { lastMarker.locations.push(rawLocation); } } this.contentElement.removeChildren(); let container = null; for (let i = 0; i < markers.length; ++i) { if (!i || markers[i].model.section() !== markers[i - 1].model.section()) { container = this.contentElement.createChild('div', 'media-inspector-marker-container'); } const marker = markers[i]; const bar = this._createElementFromMediaQueryModel(marker.model); this.elementsToMediaQueryModel.set(bar, marker.model); this.elementsToCSSLocations.set(bar, marker.locations); bar.classList.toggle('media-inspector-marker-inactive', !marker.active); if (!container) { throw new Error('Could not find container to render media queries into.'); } container.appendChild(bar); } } /** * @return {number} */ _zoomFactor() { return UI.ZoomManager.ZoomManager.instance().zoomFactor() / this._scale; } /** * @override */ wasShown() { this._scheduleMediaQueriesUpdate(); } /** * @param {!MediaQueryUIModel} model * @return {!Element} */ _createElementFromMediaQueryModel(model) { const zoomFactor = this._zoomFactor(); const minWidthExpression = model.minWidthExpression(); const maxWidthExpression = model.maxWidthExpression(); const minWidthValue = minWidthExpression ? (minWidthExpression.computedLength() || 0) / zoomFactor : 0; const maxWidthValue = maxWidthExpression ? (maxWidthExpression.computedLength() || 0) / zoomFactor : 0; const result = document.createElement('div'); result.classList.add('media-inspector-bar'); if (model.section() === Section.Max) { result.createChild('div', 'media-inspector-marker-spacer'); const markerElement = result.createChild('div', 'media-inspector-marker media-inspector-marker-max-width'); markerElement.style.width = maxWidthValue + 'px'; UI.Tooltip.Tooltip.install(markerElement, model.mediaText()); appendLabel(markerElement, model.maxWidthExpression(), false, false); appendLabel(markerElement, model.maxWidthExpression(), true, true); result.createChild('div', 'media-inspector-marker-spacer'); } if (model.section() === Section.MinMax) { result.createChild('div', 'media-inspector-marker-spacer'); const leftElement = result.createChild('div', 'media-inspector-marker media-inspector-marker-min-max-width'); leftElement.style.width = (maxWidthValue - minWidthValue) * 0.5 + 'px'; UI.Tooltip.Tooltip.install(leftElement, model.mediaText()); appendLabel(leftElement, model.maxWidthExpression(), true, false); appendLabel(leftElement, model.minWidthExpression(), false, true); result.createChild('div', 'media-inspector-marker-spacer').style.flex = '0 0 ' + minWidthValue + 'px'; const rightElement = result.createChild('div', 'media-inspector-marker media-inspector-marker-min-max-width'); rightElement.style.width = (maxWidthValue - minWidthValue) * 0.5 + 'px'; UI.Tooltip.Tooltip.install(rightElement, model.mediaText()); appendLabel(rightElement, model.minWidthExpression(), true, false); appendLabel(rightElement, model.maxWidthExpression(), false, true); result.createChild('div', 'media-inspector-marker-spacer'); } if (model.section() === Section.Min) { const leftElement = result.createChild( 'div', 'media-inspector-marker media-inspector-marker-min-width media-inspector-marker-min-width-left'); UI.Tooltip.Tooltip.install(leftElement, model.mediaText()); appendLabel(leftElement, model.minWidthExpression(), false, false); result.createChild('div', 'media-inspector-marker-spacer').style.flex = '0 0 ' + minWidthValue + 'px'; const rightElement = result.createChild( 'div', 'media-inspector-marker media-inspector-marker-min-width media-inspector-marker-min-width-right'); UI.Tooltip.Tooltip.install(rightElement, model.mediaText()); appendLabel(rightElement, model.minWidthExpression(), true, true); } /** * * @param {!Element} marker * @param {?SDK.CSSMedia.CSSMediaQueryExpression} expression * @param {boolean} atLeft * @param {boolean} leftAlign */ function appendLabel(marker, expression, atLeft, leftAlign) { if (!expression) { return; } marker .createChild( 'div', 'media-inspector-marker-label-container ' + (atLeft ? 'media-inspector-marker-label-container-left' : 'media-inspector-marker-label-container-right')) .createChild( 'span', 'media-inspector-marker-label ' + (leftAlign ? 'media-inspector-label-left' : 'media-inspector-label-right')) .textContent = expression.value() + expression.unit(); } return result; } } /** * @enum {number} */ export const Section = { Max: 0, MinMax: 1, Min: 2 }; export class MediaQueryUIModel { /** * @param {!SDK.CSSMedia.CSSMedia} cssMedia * @param {?SDK.CSSMedia.CSSMediaQueryExpression} minWidthExpression * @param {?SDK.CSSMedia.CSSMediaQueryExpression} maxWidthExpression * @param {boolean} active */ constructor(cssMedia, minWidthExpression, maxWidthExpression, active) { this._cssMedia = cssMedia; this._minWidthExpression = minWidthExpression; this._maxWidthExpression = maxWidthExpression; this._active = active; if (maxWidthExpression && !minWidthExpression) { this._section = Section.Max; } else if (minWidthExpression && maxWidthExpression) { this._section = Section.MinMax; } else { this._section = Section.Min; } } /** * @param {!SDK.CSSMedia.CSSMedia} cssMedia * @param {!SDK.CSSMedia.CSSMediaQuery} mediaQuery * @return {?MediaQueryUIModel} */ static createFromMediaQuery(cssMedia, mediaQuery) { let maxWidthExpression = null; let maxWidthPixels = Number.MAX_VALUE; let minWidthExpression = null; let minWidthPixels = Number.MIN_VALUE; const expressions = mediaQuery.expressions(); if (!expressions) { return null; } for (let i = 0; i < expressions.length; ++i) { const expression = expressions[i]; const feature = expression.feature(); if (feature.indexOf('width') === -1) { continue; } const pixels = expression.computedLength(); if (feature.startsWith('max-') && pixels && pixels < maxWidthPixels) { maxWidthExpression = expression; maxWidthPixels = pixels; } else if (feature.startsWith('min-') && pixels && pixels > minWidthPixels) { minWidthExpression = expression; minWidthPixels = pixels; } } if (minWidthPixels > maxWidthPixels || (!maxWidthExpression && !minWidthExpression)) { return null; } return new MediaQueryUIModel(cssMedia, minWidthExpression, maxWidthExpression, mediaQuery.active()); } /** * @param {!MediaQueryUIModel} other * @return {boolean} */ equals(other) { return this.compareTo(other) === 0; } /** * @param {!MediaQueryUIModel} other * @return {boolean} */ dimensionsEqual(other) { const thisMinWidthExpression = this.minWidthExpression(); const otherMinWidthExpression = other.minWidthExpression(); const thisMaxWidthExpression = this.maxWidthExpression(); const otherMaxWidthExpression = other.maxWidthExpression(); const sectionsEqual = this.section() === other.section(); // If there isn't an other min width expression, they aren't equal, so the optional chaining operator is safe to use here. const minWidthEqual = !thisMinWidthExpression || thisMinWidthExpression.computedLength() === otherMinWidthExpression?.computedLength(); const maxWidthEqual = !thisMaxWidthExpression || thisMaxWidthExpression.computedLength() === otherMaxWidthExpression?.computedLength(); return sectionsEqual && minWidthEqual && maxWidthEqual; } /** * @param {!MediaQueryUIModel} other * @return {number} */ compareTo(other) { if (this.section() !== other.section()) { return this.section() - other.section(); } if (this.dimensionsEqual(other)) { const myLocation = this.rawLocation(); const otherLocation = other.rawLocation(); if (!myLocation && !otherLocation) { return Platform.StringUtilities.compare(this.mediaText(), other.mediaText()); } if (myLocation && !otherLocation) { return 1; } if (!myLocation && otherLocation) { return -1; } if (this.active() !== other.active()) { return this.active() ? -1 : 1; } if (!myLocation || !otherLocation) { // This conditional never runs, because it's dealt with above, but // TypeScript can't follow that by this point both myLocation and // otherLocation must exist. return 0; } return Platform.StringUtilities.compare(myLocation.url, otherLocation.url) || myLocation.lineNumber - otherLocation.lineNumber || myLocation.columnNumber - otherLocation.columnNumber; } const thisMaxWidthExpression = this.maxWidthExpression(); const otherMaxWidthExpression = other.maxWidthExpression(); const thisMaxLength = thisMaxWidthExpression ? thisMaxWidthExpression.computedLength() || 0 : 0; const otherMaxLength = otherMaxWidthExpression ? otherMaxWidthExpression.computedLength() || 0 : 0; const thisMinWidthExpression = this.minWidthExpression(); const otherMinWidthExpression = other.minWidthExpression(); const thisMinLength = thisMinWidthExpression ? thisMinWidthExpression.computedLength() || 0 : 0; const otherMinLength = otherMinWidthExpression ? otherMinWidthExpression.computedLength() || 0 : 0; if (this.section() === Section.Max) { return otherMaxLength - thisMaxLength; } if (this.section() === Section.Min) { return thisMinLength - otherMinLength; } return thisMinLength - otherMinLength || otherMaxLength - thisMaxLength; } /** * @return {!Section} */ section() { return this._section; } /** * @return {string} */ mediaText() { return this._cssMedia.text || ''; } /** * @return {?SDK.CSSModel.CSSLocation} */ rawLocation() { if (!this._rawLocation) { this._rawLocation = this._cssMedia.rawLocation(); } return this._rawLocation; } /** * @return {?SDK.CSSMedia.CSSMediaQueryExpression} */ minWidthExpression() { return this._minWidthExpression; } /** * @return {?SDK.CSSMedia.CSSMediaQueryExpression} */ maxWidthExpression() { return this._maxWidthExpression; } /** * @return {boolean} */ active() { return this._active; } }