/* * Copyright (C) 2013 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WebInspector.CSSStyleManager = class CSSStyleManager extends WebInspector.Object { constructor() { super(); if (window.CSSAgent) CSSAgent.enable(); WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceAdded, this); WebInspector.Resource.addEventListener(WebInspector.SourceCode.Event.ContentDidChange, this._resourceContentDidChange, this); WebInspector.Resource.addEventListener(WebInspector.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this); WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeModified, this._nodeAttributesDidChange, this); WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeRemoved, this._nodeAttributesDidChange, this); WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.EnabledPseudoClassesChanged, this._nodePseudoClassesDidChange, this); this._colorFormatSetting = new WebInspector.Setting("default-color-format", WebInspector.Color.Format.Original); this._styleSheetIdentifierMap = new Map; this._styleSheetFrameURLMap = new Map; this._nodeStylesMap = {}; // COMPATIBILITY (iOS 9): Legacy backends did not send stylesheet // added/removed events and must be fetched manually. this._fetchedInitialStyleSheets = window.CSSAgent && window.CSSAgent.hasEvent("styleSheetAdded"); } // Static static protocolStyleSheetOriginToEnum(origin) { switch (origin) { case CSSAgent.StyleSheetOrigin.Regular: return WebInspector.CSSStyleSheet.Type.Author; case CSSAgent.StyleSheetOrigin.User: return WebInspector.CSSStyleSheet.Type.User; case CSSAgent.StyleSheetOrigin.UserAgent: return WebInspector.CSSStyleSheet.Type.UserAgent; case CSSAgent.StyleSheetOrigin.Inspector: return WebInspector.CSSStyleSheet.Type.Inspector; default: console.assert(false, "Unknown CSS.StyleSheetOrigin", origin); return CSSAgent.StyleSheetOrigin.Regular; } } static protocolMediaSourceToEnum(source) { switch (source) { case CSSAgent.CSSMediaSource.MediaRule: return WebInspector.CSSMedia.Type.MediaRule; case CSSAgent.CSSMediaSource.ImportRule: return WebInspector.CSSMedia.Type.ImportRule; case CSSAgent.CSSMediaSource.LinkedSheet: return WebInspector.CSSMedia.Type.LinkedStyleSheet; case CSSAgent.CSSMediaSource.InlineSheet: return WebInspector.CSSMedia.Type.InlineStyleSheet; default: console.assert(false, "Unknown CSS.CSSMediaSource", origin); return WebInspector.CSSMedia.Type.MediaRule; } } // Public get preferredColorFormat() { return this._colorFormatSetting.value; } get styleSheets() { return [...this._styleSheetIdentifierMap.values()]; } canForcePseudoClasses() { return window.CSSAgent && !!CSSAgent.forcePseudoState; } propertyNameHasOtherVendorPrefix(name) { if (!name || name.length < 4 || name.charAt(0) !== "-") return false; var match = name.match(/^(?:-moz-|-ms-|-o-|-epub-)/); if (!match) return false; return true; } propertyValueHasOtherVendorKeyword(value) { var match = value.match(/(?:-moz-|-ms-|-o-|-epub-)[-\w]+/); if (!match) return false; return true; } canonicalNameForPropertyName(name) { if (!name || name.length < 8 || name.charAt(0) !== "-") return name; var match = name.match(/^(?:-webkit-|-khtml-|-apple-)(.+)/); if (!match) return name; return match[1]; } fetchStyleSheetsIfNeeded() { if (this._fetchedInitialStyleSheets) return; this._fetchInfoForAllStyleSheets(function() {}); } styleSheetForIdentifier(id) { let styleSheet = this._styleSheetIdentifierMap.get(id); if (styleSheet) return styleSheet; styleSheet = new WebInspector.CSSStyleSheet(id); this._styleSheetIdentifierMap.set(id, styleSheet); return styleSheet; } stylesForNode(node) { if (node.id in this._nodeStylesMap) return this._nodeStylesMap[node.id]; var styles = new WebInspector.DOMNodeStyles(node); this._nodeStylesMap[node.id] = styles; return styles; } preferredInspectorStyleSheetForFrame(frame, callback) { var inspectorStyleSheets = this._inspectorStyleSheetsForFrame(frame); for (let styleSheet of inspectorStyleSheets) { if (styleSheet[WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol]) { callback(styleSheet); return; } } if (CSSAgent.createStyleSheet) { CSSAgent.createStyleSheet(frame.id, function(error, styleSheetId) { let styleSheet = WebInspector.cssStyleManager.styleSheetForIdentifier(styleSheetId); styleSheet[WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol] = true; callback(styleSheet); }); return; } // COMPATIBILITY (iOS 9): CSS.createStyleSheet did not exist. // Legacy backends can only create the Inspector StyleSheet through CSS.addRule. // Exploit that to create the Inspector StyleSheet for the document.body node in // this frame, then get the StyleSheet for the new rule. let expression = appendWebInspectorSourceURL("document"); let contextId = frame.pageExecutionContext.id; RuntimeAgent.evaluate.invoke({expression, objectGroup: "", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, contextId, returnByValue: false, generatePreview: false}, documentAvailable); function documentAvailable(error, documentRemoteObjectPayload) { if (error) { callback(null); return; } let remoteObject = WebInspector.RemoteObject.fromPayload(documentRemoteObjectPayload); remoteObject.pushNodeToFrontend(documentNodeAvailable.bind(null, remoteObject)); } function documentNodeAvailable(remoteObject, documentNodeId) { remoteObject.release(); if (!documentNodeId) { callback(null); return; } DOMAgent.querySelector(documentNodeId, "body", bodyNodeAvailable); } function bodyNodeAvailable(error, bodyNodeId) { if (error) { console.error(error); callback(null); return; } let selector = ""; // Intentionally empty. CSSAgent.addRule(bodyNodeId, selector, cssRuleAvailable); } function cssRuleAvailable(error, payload) { if (error || !payload.ruleId) { callback(null); return; } let styleSheetId = payload.ruleId.styleSheetId; let styleSheet = WebInspector.cssStyleManager.styleSheetForIdentifier(styleSheetId); if (!styleSheet) { callback(null); return; } styleSheet[WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol] = true; console.assert(styleSheet.isInspectorStyleSheet()); console.assert(styleSheet.parentFrame === frame); callback(styleSheet); } } // Protected mediaQueryResultChanged() { // Called from WebInspector.CSSObserver. for (var key in this._nodeStylesMap) this._nodeStylesMap[key].mediaQueryResultDidChange(); } styleSheetChanged(styleSheetIdentifier) { // Called from WebInspector.CSSObserver. var styleSheet = this.styleSheetForIdentifier(styleSheetIdentifier); console.assert(styleSheet); // Do not observe inline styles if (styleSheet.isInlineStyleAttributeStyleSheet()) return; styleSheet.noteContentDidChange(); this._updateResourceContent(styleSheet); } styleSheetAdded(styleSheetInfo) { console.assert(!this._styleSheetIdentifierMap.has(styleSheetInfo.styleSheetId), "Attempted to add a CSSStyleSheet but identifier was already in use"); let styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId); let parentFrame = WebInspector.frameResourceManager.frameForIdentifier(styleSheetInfo.frameId); let origin = WebInspector.CSSStyleManager.protocolStyleSheetOriginToEnum(styleSheetInfo.origin); styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame, origin, styleSheetInfo.isInline, styleSheetInfo.startLine, styleSheetInfo.startColumn); this.dispatchEventToListeners(WebInspector.CSSStyleManager.Event.StyleSheetAdded, {styleSheet}); } styleSheetRemoved(styleSheetIdentifier) { let styleSheet = this._styleSheetIdentifierMap.get(styleSheetIdentifier); console.assert(styleSheet, "Attempted to remove a CSSStyleSheet that was not tracked"); if (!styleSheet) return; this._styleSheetIdentifierMap.delete(styleSheetIdentifier); this.dispatchEventToListeners(WebInspector.CSSStyleManager.Event.StyleSheetRemoved, {styleSheet}); } // Private _inspectorStyleSheetsForFrame(frame) { let styleSheets = []; for (let styleSheet of this.styleSheets) { if (styleSheet.isInspectorStyleSheet() && styleSheet.parentFrame === frame) styleSheets.push(styleSheet); } return styleSheets; } _nodePseudoClassesDidChange(event) { var node = event.target; for (var key in this._nodeStylesMap) { var nodeStyles = this._nodeStylesMap[key]; if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node)) continue; nodeStyles.pseudoClassesDidChange(node); } } _nodeAttributesDidChange(event) { var node = event.target; for (var key in this._nodeStylesMap) { var nodeStyles = this._nodeStylesMap[key]; if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node)) continue; nodeStyles.attributeDidChange(node, event.data.name); } } _mainResourceDidChange(event) { console.assert(event.target instanceof WebInspector.Frame); if (!event.target.isMainFrame()) return; // Clear our maps when the main frame navigates. this._fetchedInitialStyleSheets = window.CSSAgent && window.CSSAgent.hasEvent("styleSheetAdded"); this._styleSheetIdentifierMap.clear(); this._styleSheetFrameURLMap.clear(); this._nodeStylesMap = {}; } _resourceAdded(event) { console.assert(event.target instanceof WebInspector.Frame); var resource = event.data.resource; console.assert(resource); if (resource.type !== WebInspector.Resource.Type.Stylesheet) return; this._clearStyleSheetsForResource(resource); } _resourceTypeDidChange(event) { console.assert(event.target instanceof WebInspector.Resource); var resource = event.target; if (resource.type !== WebInspector.Resource.Type.Stylesheet) return; this._clearStyleSheetsForResource(resource); } _clearStyleSheetsForResource(resource) { // Clear known stylesheets for this URL and frame. This will cause the stylesheets to // be updated next time _fetchInfoForAllStyleSheets is called. this._styleSheetIdentifierMap.delete(this._frameURLMapKey(resource.parentFrame, resource.url)); } _frameURLMapKey(frame, url) { return frame.id + ":" + url; } _lookupStyleSheetForResource(resource, callback) { this._lookupStyleSheet(resource.parentFrame, resource.url, callback); } _lookupStyleSheet(frame, url, callback) { console.assert(frame instanceof WebInspector.Frame); let key = this._frameURLMapKey(frame, url); function styleSheetsFetched() { callback(this._styleSheetFrameURLMap.get(key) || null); } let styleSheet = this._styleSheetFrameURLMap.get(key) || null; if (styleSheet) callback(styleSheet); else this._fetchInfoForAllStyleSheets(styleSheetsFetched.bind(this)); } _fetchInfoForAllStyleSheets(callback) { console.assert(typeof callback === "function"); function processStyleSheets(error, styleSheets) { this._styleSheetFrameURLMap.clear(); if (error) { callback(); return; } for (let styleSheetInfo of styleSheets) { let parentFrame = WebInspector.frameResourceManager.frameForIdentifier(styleSheetInfo.frameId); let origin = WebInspector.CSSStyleManager.protocolStyleSheetOriginToEnum(styleSheetInfo.origin); // COMPATIBILITY (iOS 9): The info did not have 'isInline', 'startLine', and 'startColumn', so make false and 0 in these cases. let isInline = styleSheetInfo.isInline || false; let startLine = styleSheetInfo.startLine || 0; let startColumn = styleSheetInfo.startColumn || 0; let styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId); styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame, origin, isInline, startLine, startColumn); let key = this._frameURLMapKey(parentFrame, styleSheetInfo.sourceURL); this._styleSheetFrameURLMap.set(key, styleSheet); } callback(); } CSSAgent.getAllStyleSheets(processStyleSheets.bind(this)); } _resourceContentDidChange(event) { var resource = event.target; if (resource === this._ignoreResourceContentDidChangeEventForResource) return; // Ignore if it isn't a CSS stylesheet. if (resource.type !== WebInspector.Resource.Type.Stylesheet || resource.syntheticMIMEType !== "text/css") return; function applyStyleSheetChanges() { function styleSheetFound(styleSheet) { resource.__pendingChangeTimeout = undefined; console.assert(styleSheet); if (!styleSheet) return; // To prevent updating a TextEditor's content while the user is typing in it we want to // ignore the next _updateResourceContent call. resource.__ignoreNextUpdateResourceContent = true; WebInspector.branchManager.currentBranch.revisionForRepresentedObject(styleSheet).content = resource.content; } this._lookupStyleSheetForResource(resource, styleSheetFound.bind(this)); } if (resource.__pendingChangeTimeout) clearTimeout(resource.__pendingChangeTimeout); resource.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500); } _updateResourceContent(styleSheet) { console.assert(styleSheet); function fetchedStyleSheetContent(parameters) { var styleSheet = parameters.sourceCode; var content = parameters.content; styleSheet.__pendingChangeTimeout = undefined; console.assert(styleSheet.url); if (!styleSheet.url) return; var resource = styleSheet.parentFrame.resourceForURL(styleSheet.url);; if (!resource) return; // Only try to update stylesheet resources. Other resources, like documents, can contain // multiple stylesheets and we don't have the source ranges to update those. if (resource.type !== WebInspector.Resource.Type.Stylesheet) return; if (resource.__ignoreNextUpdateResourceContent) { resource.__ignoreNextUpdateResourceContent = false; return; } this._ignoreResourceContentDidChangeEventForResource = resource; WebInspector.branchManager.currentBranch.revisionForRepresentedObject(resource).content = content; this._ignoreResourceContentDidChangeEventForResource = null; } function styleSheetReady() { styleSheet.requestContent().then(fetchedStyleSheetContent.bind(this)); } function applyStyleSheetChanges() { if (styleSheet.url) styleSheetReady.call(this); else this._fetchInfoForAllStyleSheets(styleSheetReady.bind(this)); } if (styleSheet.__pendingChangeTimeout) clearTimeout(styleSheet.__pendingChangeTimeout); styleSheet.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500); } }; WebInspector.CSSStyleManager.Event = { StyleSheetAdded: "css-style-manager-style-sheet-added", StyleSheetRemoved: "css-style-manager-style-sheet-removed", }; WebInspector.CSSStyleManager.PseudoElementNames = ["before", "after"]; WebInspector.CSSStyleManager.ForceablePseudoClasses = ["active", "focus", "hover", "visited"]; WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol = Symbol("css-style-manager-preferred-inspector-stylesheet");