1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.CSSStyleManager = function()
27{
28    WebInspector.Object.call(this);
29
30    if (window.CSSAgent)
31        CSSAgent.enable();
32
33    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
34    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceAdded, this);
35    WebInspector.Resource.addEventListener(WebInspector.SourceCode.Event.ContentDidChange, this._resourceContentDidChange, this);
36    WebInspector.Resource.addEventListener(WebInspector.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this);
37
38    WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeModified, this._nodeAttributesDidChange, this);
39    WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeRemoved, this._nodeAttributesDidChange, this);
40    WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.EnabledPseudoClassesChanged, this._nodePseudoClassesDidChange, this);
41
42    this._colorFormatSetting = new WebInspector.Setting("default-color-format", WebInspector.Color.Format.Original);
43
44    this._styleSheetIdentifierMap = {};
45    this._styleSheetFrameURLMap = {};
46    this._nodeStylesMap = {};
47}
48
49WebInspector.CSSStyleManager.ForceablePseudoClasses = ["active", "focus", "hover", "visited"];
50
51WebInspector.CSSStyleManager.prototype = {
52    constructor: WebInspector.CSSStyleManager,
53
54    // Public
55
56    get preferredColorFormat()
57    {
58        return this._colorFormatSetting.value;
59    },
60
61    canForcePseudoClasses: function()
62    {
63        return window.CSSAgent && !!CSSAgent.forcePseudoState;
64    },
65
66    propertyNameHasOtherVendorPrefix: function(name)
67    {
68        if (!name || name.length < 4 || name.charAt(0) !== "-")
69            return false;
70
71        var match = name.match(/^(?:-moz-|-ms-|-o-|-epub-)/);
72        if (!match)
73            return false;
74
75        return true;
76    },
77
78    propertyValueHasOtherVendorKeyword: function(value)
79    {
80        var match = value.match(/(?:-moz-|-ms-|-o-|-epub-)[-\w]+/);
81        if (!match)
82            return false;
83
84        return true;
85    },
86
87    canonicalNameForPropertyName: function(name)
88    {
89        if (!name || name.length < 8 || name.charAt(0) !== "-")
90            return name;
91
92        var match = name.match(/^(?:-webkit-|-khtml-|-apple-)(.+)/);
93        if (!match)
94            return name;
95
96        return match[1];
97    },
98
99    styleSheetForIdentifier: function(id)
100    {
101        if (id in this._styleSheetIdentifierMap)
102            return this._styleSheetIdentifierMap[id];
103
104        var styleSheet = new WebInspector.CSSStyleSheet(id);
105        this._styleSheetIdentifierMap[id] = styleSheet;
106        return styleSheet;
107    },
108
109    stylesForNode: function(node)
110    {
111        if (node.id in this._nodeStylesMap)
112            return this._nodeStylesMap[node.id];
113
114        var styles = new WebInspector.DOMNodeStyles(node);
115        this._nodeStylesMap[node.id] = styles;
116        return styles;
117    },
118
119    // Protected
120
121    mediaQueryResultChanged: function()
122    {
123        // Called from WebInspector.CSSObserver.
124
125        for (var key in this._nodeStylesMap)
126            this._nodeStylesMap[key].mediaQueryResultDidChange();
127    },
128
129    styleSheetChanged: function(styleSheetIdentifier)
130    {
131        // Called from WebInspector.CSSObserver.
132
133        var styleSheet = this.styleSheetForIdentifier(styleSheetIdentifier);
134        console.assert(styleSheet);
135
136        styleSheet.noteContentDidChange();
137
138        this._updateResourceContent(styleSheet);
139    },
140
141    // Private
142
143    _nodePseudoClassesDidChange: function(event)
144    {
145        var node = event.target;
146
147        for (var key in this._nodeStylesMap) {
148            var nodeStyles = this._nodeStylesMap[key];
149            if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node))
150                continue;
151            nodeStyles.pseudoClassesDidChange(node);
152        }
153    },
154
155    _nodeAttributesDidChange: function(event)
156    {
157        var node = event.target;
158
159        for (var key in this._nodeStylesMap) {
160            var nodeStyles = this._nodeStylesMap[key];
161            if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node))
162                continue;
163            nodeStyles.attributeDidChange(node, event.data.name);
164        }
165    },
166
167    _mainResourceDidChange: function(event)
168    {
169        console.assert(event.target instanceof WebInspector.Frame);
170
171        if (!event.target.isMainFrame())
172            return;
173
174        // Clear our maps when the main frame navigates.
175
176        this._styleSheetIdentifierMap = {};
177        this._styleSheetFrameURLMap = {};
178        this._nodeStylesMap = {};
179    },
180
181    _resourceAdded: function(event)
182    {
183        console.assert(event.target instanceof WebInspector.Frame);
184
185        var resource = event.data.resource;
186        console.assert(resource);
187
188        if (resource.type !== WebInspector.Resource.Type.Stylesheet)
189            return;
190
191        this._clearStyleSheetsForResource(resource);
192    },
193
194    _resourceTypeDidChange: function(event)
195    {
196        console.assert(event.target instanceof WebInspector.Resource);
197
198        var resource = event.target;
199        if (resource.type !== WebInspector.Resource.Type.Stylesheet)
200            return;
201
202        this._clearStyleSheetsForResource(resource);
203    },
204
205    _clearStyleSheetsForResource: function(resource)
206    {
207        // Clear known stylesheets for this URL and frame. This will cause the stylesheets to
208        // be updated next time _fetchInfoForAllStyleSheets is called.
209        // COMPATIBILITY (iOS 6): The frame's id was not available for the key, so delete just the url too.
210        delete this._styleSheetFrameURLMap[this._frameURLMapKey(resource.parentFrame, resource.url)];
211        delete this._styleSheetFrameURLMap[resource.url];
212    },
213
214    _frameURLMapKey: function(frame, url)
215    {
216        return (frame ? frame.id + ":" : "") + url;
217    },
218
219    _lookupStyleSheetForResource: function(resource, callback)
220    {
221        this._lookupStyleSheet(resource.parentFrame, resource.url, callback);
222    },
223
224    _lookupStyleSheet: function(frame, url, callback)
225    {
226        console.assert(frame instanceof WebInspector.Frame);
227
228        function syleSheetsFetched()
229        {
230            callback(this._styleSheetFrameURLMap[key] || this._styleSheetFrameURLMap[url] || null);
231        }
232
233        var key = this._frameURLMapKey(frame, url);
234
235        // COMPATIBILITY (iOS 6): The frame's id was not available for the key, so check for just the url too.
236        if (key in this._styleSheetFrameURLMap || url in this._styleSheetFrameURLMap)
237            callback(this._styleSheetFrameURLMap[key] || this._styleSheetFrameURLMap[url] || null);
238        else
239            this._fetchInfoForAllStyleSheets(syleSheetsFetched.bind(this));
240    },
241
242    _fetchInfoForAllStyleSheets: function(callback)
243    {
244        console.assert(typeof callback === "function");
245
246        function processStyleSheets(error, styleSheets)
247        {
248            this._styleSheetFrameURLMap = {};
249
250            if (error) {
251                callback();
252                return;
253            }
254
255            for (var i = 0; i < styleSheets.length; ++i) {
256                var styleSheetInfo = styleSheets[i];
257
258                // COMPATIBILITY (iOS 6): The info did not have 'frameId', so make parentFrame null in that case.
259                var parentFrame = "frameId" in styleSheetInfo ? WebInspector.frameResourceManager.frameForIdentifier(styleSheetInfo.frameId) : null;
260
261                var styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId);
262                styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame);
263
264                var key = this._frameURLMapKey(parentFrame, styleSheetInfo.sourceURL);
265                this._styleSheetFrameURLMap[key] = styleSheet;
266            }
267
268            callback();
269        }
270
271        CSSAgent.getAllStyleSheets(processStyleSheets.bind(this));
272    },
273
274    _resourceContentDidChange: function(event)
275    {
276        var resource = event.target;
277        if (resource === this._ignoreResourceContentDidChangeEventForResource)
278            return;
279
280        // Ignore if it isn't a CSS stylesheet.
281        if (resource.type !== WebInspector.Resource.Type.Stylesheet || resource.syntheticMIMEType !== "text/css")
282            return;
283
284        function applyStyleSheetChanges()
285        {
286            function styleSheetFound(styleSheet)
287            {
288                delete resource.__pendingChangeTimeout;
289
290                console.assert(styleSheet);
291                if (!styleSheet)
292                    return;
293
294                // To prevent updating a TextEditor's content while the user is typing in it we want to
295                // ignore the next _updateResourceContent call.
296                resource.__ignoreNextUpdateResourceContent = true;
297
298                WebInspector.branchManager.currentBranch.revisionForRepresentedObject(styleSheet).content = resource.content;
299            }
300
301            this._lookupStyleSheetForResource(resource, styleSheetFound.bind(this));
302        }
303
304        if (resource.__pendingChangeTimeout)
305            clearTimeout(resource.__pendingChangeTimeout);
306        resource.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500);
307    },
308
309    _updateResourceContent: function(styleSheet)
310    {
311        console.assert(styleSheet);
312
313        function fetchedStyleSheetContent(styleSheet, content)
314        {
315            delete styleSheet.__pendingChangeTimeout;
316
317            console.assert(styleSheet.url);
318            if (!styleSheet.url)
319                return;
320
321            var resource = null;
322
323            // COMPATIBILITY (iOS 6): The stylesheet did not always have a frame, so fallback to looking
324            // for the resource in all frames.
325            if (styleSheet.parentFrame)
326                resource = styleSheet.parentFrame.resourceForURL(styleSheet.url);
327            else
328                resource = WebInspector.frameResourceManager.resourceForURL(styleSheet.url);
329
330            if (!resource)
331                return;
332
333            // Only try to update stylesheet resources. Other resources, like documents, can contain
334            // multiple stylesheets and we don't have the source ranges to update those.
335            if (resource.type !== WebInspector.Resource.Type.Stylesheet)
336                return;
337
338            if (resource.__ignoreNextUpdateResourceContent) {
339                delete resource.__ignoreNextUpdateResourceContent;
340                return;
341            }
342
343            this._ignoreResourceContentDidChangeEventForResource = resource;
344            WebInspector.branchManager.currentBranch.revisionForRepresentedObject(resource).content = content;
345            delete this._ignoreResourceContentDidChangeEventForResource;
346        }
347
348        function styleSheetReady()
349        {
350            styleSheet.requestContent(fetchedStyleSheetContent.bind(this));
351        }
352
353        function applyStyleSheetChanges()
354        {
355            if (styleSheet.url)
356                styleSheetReady.call(this);
357            else
358                this._fetchInfoForAllStyleSheets(styleSheetReady.bind(this));
359        }
360
361        if (styleSheet.__pendingChangeTimeout)
362            clearTimeout(styleSheet.__pendingChangeTimeout);
363        styleSheet.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500);
364    }
365}
366
367WebInspector.CSSStyleManager.prototype.__proto__ = WebInspector.Object.prototype;
368