Files
react-native-devtools-frontend/front_end/bindings/CSSWorkspaceBinding.js
T
chait pinnamaneni f09b0b1130 Inline stylesheet location update for elements pane
Inline stylesheets are not correctly linked in styles pane if document source url is different from root domain url for multi-page applications.


Before: https://i.imgur.com/M0t7WTy.png
After: https://i.imgur.com/WsOdosd.png


Bug:1133930
Change-Id: I347be9b0e80235a5929f688092fcac5c3ea8cf94
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2441882
Commit-Queue: Chait Pinnamaneni <cpinnamaneni@microsoft.com>
Reviewed-by: Michael Liao <michael.liao@microsoft.com>
Reviewed-by: Shane Clifford <shanejc@microsoft.com>
Reviewed-by: Robert Paveza <Rob.Paveza@microsoft.com>
2020-10-07 18:30:16 +00:00

411 lines
12 KiB
JavaScript

// 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 Common from '../common/common.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js';
import * as Workspace from '../workspace/workspace.js'; // eslint-disable-line no-unused-vars
import {LiveLocation as LiveLocationInterface, LiveLocationPool, LiveLocationWithPool,} from './LiveLocation.js'; // eslint-disable-line no-unused-vars
import {ResourceMapping} from './ResourceMapping.js';
import {SASSSourceMapping} from './SASSSourceMapping.js';
import {StylesSourceMapping} from './StylesSourceMapping.js';
/**
* @type {!CSSWorkspaceBinding}
*/
let cssWorkspaceBindingInstance;
/**
* @implements {SDK.SDKModel.SDKModelObserver<!SDK.CSSModel.CSSModel>}
*/
export class CSSWorkspaceBinding {
/**
* @private
* @param {!SDK.SDKModel.TargetManager} targetManager
* @param {!Workspace.Workspace.WorkspaceImpl} workspace
*/
constructor(targetManager, workspace) {
this._workspace = workspace;
/** @type {!Map.<!SDK.CSSModel.CSSModel, !ModelInfo>} */
this._modelToInfo = new Map();
/** @type {!Array<!SourceMapping>} */
this._sourceMappings = [];
targetManager.observeModels(SDK.CSSModel.CSSModel, this);
/** @type {!Set.<!Promise<?>>} */
this._liveLocationPromises = new Set();
}
/**
* @param {{forceNew: ?boolean, targetManager: ?SDK.SDKModel.TargetManager, workspace: ?Workspace.Workspace.WorkspaceImpl}} opts
*/
static instance(opts = {forceNew: null, targetManager: null, workspace: null}) {
const {forceNew, targetManager, workspace} = opts;
if (!cssWorkspaceBindingInstance || forceNew) {
if (!targetManager || !workspace) {
throw new Error(
`Unable to create settings: targetManager and workspace must be provided: ${new Error().stack}`);
}
cssWorkspaceBindingInstance = new CSSWorkspaceBinding(targetManager, workspace);
}
return cssWorkspaceBindingInstance;
}
/**
* @param {!SDK.CSSModel.CSSModel} cssModel
* @return {!ModelInfo}
*/
_getCSSModelInfo(cssModel) {
return /** @type {!ModelInfo} */ (this._modelToInfo.get(cssModel));
}
/**
* @override
* @param {!SDK.CSSModel.CSSModel} cssModel
*/
modelAdded(cssModel) {
this._modelToInfo.set(cssModel, new ModelInfo(cssModel, this._workspace));
}
/**
* @override
* @param {!SDK.CSSModel.CSSModel} cssModel
*/
modelRemoved(cssModel) {
this._getCSSModelInfo(cssModel)._dispose();
this._modelToInfo.delete(cssModel);
}
/**
* The promise returned by this function is resolved once all *currently*
* pending LiveLocations are processed.
*
* @return {!Promise<?>}
*/
async pendingLiveLocationChangesPromise() {
await Promise.all(this._liveLocationPromises);
}
/**
* @param {!Promise<?>} promise
*/
_recordLiveLocationChange(promise) {
promise.then(() => {
this._liveLocationPromises.delete(promise);
});
this._liveLocationPromises.add(promise);
}
/**
* @param {!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader} header
* @return {!Promise<void>}
*/
async updateLocations(header) {
const updatePromise = this._getCSSModelInfo(header.cssModel())._updateLocations(header);
this._recordLiveLocationChange(updatePromise);
await updatePromise;
}
/**
* @param {!SDK.CSSModel.CSSLocation} rawLocation
* @param {function(!LiveLocationInterface):!Promise<void>} updateDelegate
* @param {!LiveLocationPool} locationPool
* @return {!Promise<!LiveLocation>}
*/
createLiveLocation(rawLocation, updateDelegate, locationPool) {
const locationPromise =
this._getCSSModelInfo(rawLocation.cssModel())._createLiveLocation(rawLocation, updateDelegate, locationPool);
this._recordLiveLocationChange(locationPromise);
return locationPromise;
}
/**
* @param {!SDK.CSSProperty.CSSProperty} cssProperty
* @param {boolean} forName
* @return {?Workspace.UISourceCode.UILocation}
*/
propertyUILocation(cssProperty, forName) {
const style = cssProperty.ownerStyle;
if (!style || style.type !== SDK.CSSStyleDeclaration.Type.Regular || !style.styleSheetId) {
return null;
}
const header = style.cssModel().styleSheetHeaderForId(style.styleSheetId);
if (!header) {
return null;
}
const range = forName ? cssProperty.nameRange() : cssProperty.valueRange();
if (!range) {
return null;
}
const lineNumber = range.startLine;
const columnNumber = range.startColumn;
const rawLocation = new SDK.CSSModel.CSSLocation(
header, header.lineNumberInSource(lineNumber), header.columnNumberInSource(lineNumber, columnNumber));
return this.rawLocationToUILocation(rawLocation);
}
/**
* @param {!SDK.CSSModel.CSSLocation} rawLocation
* @return {?Workspace.UISourceCode.UILocation}
*/
rawLocationToUILocation(rawLocation) {
for (let i = this._sourceMappings.length - 1; i >= 0; --i) {
const uiLocation = this._sourceMappings[i].rawLocationToUILocation(rawLocation);
if (uiLocation) {
return uiLocation;
}
}
return this._getCSSModelInfo(rawLocation.cssModel())._rawLocationToUILocation(rawLocation);
}
/**
* @param {!Workspace.UISourceCode.UILocation} uiLocation
* @return {!Array<!SDK.CSSModel.CSSLocation>}
*/
uiLocationToRawLocations(uiLocation) {
for (let i = this._sourceMappings.length - 1; i >= 0; --i) {
const rawLocations = this._sourceMappings[i].uiLocationToRawLocations(uiLocation);
if (rawLocations.length) {
return rawLocations;
}
}
const rawLocations = [];
for (const modelInfo of this._modelToInfo.values()) {
rawLocations.push(...modelInfo._uiLocationToRawLocations(uiLocation));
}
return rawLocations;
}
/**
* @param {!SourceMapping} sourceMapping
*/
addSourceMapping(sourceMapping) {
this._sourceMappings.push(sourceMapping);
}
}
/**
* @interface
*/
export class SourceMapping {
/**
* @param {!SDK.CSSModel.CSSLocation} rawLocation
* @return {?Workspace.UISourceCode.UILocation}
*/
rawLocationToUILocation(rawLocation) {
throw new Error('Not implemented yet');
}
/**
* @param {!Workspace.UISourceCode.UILocation} uiLocation
* @return {!Array<!SDK.CSSModel.CSSLocation>}
*/
uiLocationToRawLocations(uiLocation) {
throw new Error('Not implemented yet');
}
}
export class ModelInfo {
/**
* @param {!SDK.CSSModel.CSSModel} cssModel
* @param {!Workspace.Workspace.WorkspaceImpl} workspace
*/
constructor(cssModel, workspace) {
this._eventListeners = [
cssModel.addEventListener(
SDK.CSSModel.Events.StyleSheetAdded,
event => {
this._styleSheetAdded(event);
},
this),
cssModel.addEventListener(
SDK.CSSModel.Events.StyleSheetRemoved,
event => {
this._styleSheetRemoved(event);
},
this)
];
this._stylesSourceMapping = new StylesSourceMapping(cssModel, workspace);
const sourceMapManager = cssModel.sourceMapManager();
this._sassSourceMapping = new SASSSourceMapping(cssModel.target(), sourceMapManager, workspace);
/** @type {!Platform.Multimap<!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, !LiveLocation>} */
this._locations = new Platform.Multimap();
/** @type {!Platform.Multimap<string, !LiveLocation>} */
this._unboundLocations = new Platform.Multimap();
}
/**
* @param {!SDK.CSSModel.CSSLocation} rawLocation
* @param {function(!LiveLocationInterface):!Promise<void>} updateDelegate
* @param {!LiveLocationPool} locationPool
* @return {!Promise<!LiveLocation>}
*/
async _createLiveLocation(rawLocation, updateDelegate, locationPool) {
const location = new LiveLocation(rawLocation, this, updateDelegate, locationPool);
const header = rawLocation.header();
if (header) {
location._header = header;
this._locations.set(header, location);
await location.update();
} else {
this._unboundLocations.set(rawLocation.url, location);
}
return location;
}
/**
* @param {!LiveLocation} location
*/
_disposeLocation(location) {
if (location._header) {
this._locations.delete(location._header, location);
} else {
this._unboundLocations.delete(location._url, location);
}
}
/**
* @param {!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader} header
*/
_updateLocations(header) {
const promises = [];
for (const location of this._locations.get(header)) {
promises.push(location.update());
}
return Promise.all(promises);
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
async _styleSheetAdded(event) {
const header = /** @type {!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader} */ (event.data);
if (!header.sourceURL) {
return;
}
const promises = [];
for (const location of this._unboundLocations.get(header.sourceURL)) {
location._header = header;
this._locations.set(header, location);
promises.push(location.update());
}
await Promise.all(promises);
this._unboundLocations.deleteAll(header.sourceURL);
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
async _styleSheetRemoved(event) {
const header = /** @type {!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader} */ (event.data);
const promises = [];
for (const location of this._locations.get(header)) {
location._header = null;
this._unboundLocations.set(location._url, location);
promises.push(location.update());
}
await Promise.all(promises);
this._locations.deleteAll(header);
}
/**
* @param {!SDK.CSSModel.CSSLocation} rawLocation
* @return {?Workspace.UISourceCode.UILocation}
*/
_rawLocationToUILocation(rawLocation) {
let uiLocation = null;
uiLocation = uiLocation || this._sassSourceMapping.rawLocationToUILocation(rawLocation);
uiLocation = uiLocation || this._stylesSourceMapping.rawLocationToUILocation(rawLocation);
uiLocation = uiLocation || ResourceMapping.instance().cssLocationToUILocation(rawLocation);
return uiLocation;
}
/**
* @param {!Workspace.UISourceCode.UILocation} uiLocation
* @return {!Array<!SDK.CSSModel.CSSLocation>}
*/
_uiLocationToRawLocations(uiLocation) {
let rawLocations = this._sassSourceMapping.uiLocationToRawLocations(uiLocation);
if (rawLocations.length) {
return rawLocations;
}
rawLocations = this._stylesSourceMapping.uiLocationToRawLocations(uiLocation);
if (rawLocations.length) {
return rawLocations;
}
return ResourceMapping.instance().uiLocationToCSSLocations(uiLocation);
}
_dispose() {
Common.EventTarget.EventTarget.removeEventListeners(this._eventListeners);
this._stylesSourceMapping.dispose();
this._sassSourceMapping.dispose();
}
}
/**
* @unrestricted
*/
export class LiveLocation extends LiveLocationWithPool {
/**
* @param {!SDK.CSSModel.CSSLocation} rawLocation
* @param {!ModelInfo} info
* @param {function(!LiveLocationInterface):!Promise<void>} updateDelegate
* @param {!LiveLocationPool} locationPool
*/
constructor(rawLocation, info, updateDelegate, locationPool) {
super(updateDelegate, locationPool);
this._url = rawLocation.url;
this._lineNumber = rawLocation.lineNumber;
this._columnNumber = rawLocation.columnNumber;
this._info = info;
/** @type {?SDK.CSSStyleSheetHeader.CSSStyleSheetHeader} */
this._header = null;
}
/**
* @return {?SDK.CSSStyleSheetHeader.CSSStyleSheetHeader}
*/
header() {
return this._header;
}
/**
* @override
* @return {!Promise<?Workspace.UISourceCode.UILocation>}
*/
async uiLocation() {
if (!this._header) {
return null;
}
const rawLocation = new SDK.CSSModel.CSSLocation(this._header, this._lineNumber, this._columnNumber);
return CSSWorkspaceBinding.instance().rawLocationToUILocation(rawLocation);
}
/**
* @override
*/
dispose() {
super.dispose();
this._info._disposeLocation(this);
}
/**
* @override
* @return {!Promise<boolean>}
*/
async isBlackboxed() {
return false;
}
}