Web Inspector: Styles Redesign: Editing selector should not hide the rule
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 21 Mar 2019 00:22:06 +0000 (00:22 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 21 Mar 2019 00:22:06 +0000 (00:22 +0000)
https://bugs.webkit.org/show_bug.cgi?id=178489
<rdar://problem/35062434>

Reviewed by Timothy Hatcher.

Source/WebInspectorUI:

Extracts the selector payload parsing logic inside `WI.DOMNodeStyles` into static functions
so that when the user changes the selector of a `WI.CSSRule`, it's able to process and
update itself with the new selector. This is mainly useful in the case where the `WI.CSSRule`
no longer applies to the selected node (meaning it won't be part of that node's
`WI.DOMNodeStyles`) in that it allows the `WI.SpreadsheetCSSStyleDeclarationSection` to
display the new selector text and the owner `WI.SpreadsheetRulesStyleDetailsPanel` to keep
that section visible even though it isn't applicable to the current node anymore.

* UserInterface/Models/DOMNodeStyles.js:
(WI.DOMNodeStyles):
(WI.DOMNodeStyles.parseSelectorListPayload): Added.
(WI.DOMNodeStyles.createSourceCodeLocation): Added.
(WI.DOMNodeStyles.prototype.refresh):
(WI.DOMNodeStyles.prototype.refresh.fetchedMatchedStyles):
(WI.DOMNodeStyles.prototype.refresh.fetchedInlineStyles):
(WI.DOMNodeStyles.prototype.refresh.fetchedComputedStyle):
(WI.DOMNodeStyles.prototype._parseStyleDeclarationPayload):
(WI.DOMNodeStyles.prototype._parseRulePayload):
(WI.DOMNodeStyles.prototype._styleSheetContentDidChange):
(WI.DOMNodeStyles.prototype.refresh.parseRuleMatchArrayPayload): Deleted.
(WI.DOMNodeStyles.prototype._createSourceCodeLocation): Deleted.
(WI.DOMNodeStyles.prototype._parseSelectorListPayload): Deleted.
Keep track of all `WI.CSSRule` and `WI.CSSStyleDeclaration` that have ever been associated
with this object, so that if a rule's selector is changed to no longer match, and then is
changed back to match again, we are able to update that rule instead of creating a new one.

* UserInterface/Views/SpreadsheetRulesStyleDetailsPanel.js:
(WI.SpreadsheetRulesStyleDetailsPanel.prototype.layout):
(WI.SpreadsheetRulesStyleDetailsPanel.prototype._handleSectionFilterApplied):
(WI.SpreadsheetRulesStyleDetailsPanel.prototype._handleSectionSelectorWillChange): Added.
Attempt to preserve the position of any sections that are changed and no longer apply to the
current node.

* UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.js:
(WI.SpreadsheetCSSStyleDeclarationSection.prototype.spreadsheetSelectorFieldDidChange):
(WI.SpreadsheetCSSStyleDeclarationSection.prototype._renderSelector):
Drive-by: remove unused CSS classes.
* UserInterface/Models/CSSRule.js:
(WI.CSSRule.prototype.update):
(WI.CSSRule.prototype._selectorResolved):
Drive-by: remove unused event.
* UserInterface/Base/Multimap.js:
(Multimap.prototype.has): Added.
(Multimap.prototype.sets): Added.
(Multimap.prototype.copy): Added.

LayoutTests:

* inspector/unit-tests/multimap.html: Added.
* inspector/unit-tests/multimap-expected.txt: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@243264 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/multimap-expected.txt [new file with mode: 0644]
LayoutTests/inspector/unit-tests/multimap.html [new file with mode: 0644]
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Base/Multimap.js
Source/WebInspectorUI/UserInterface/Models/CSSRule.js
Source/WebInspectorUI/UserInterface/Models/DOMNodeStyles.js
Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.js
Source/WebInspectorUI/UserInterface/Views/SpreadsheetRulesStyleDetailsPanel.js

index c4c1870..fbcbe84 100644 (file)
@@ -1,3 +1,14 @@
+2019-03-20  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Styles Redesign: Editing selector should not hide the rule
+        https://bugs.webkit.org/show_bug.cgi?id=178489
+        <rdar://problem/35062434>
+
+        Reviewed by Timothy Hatcher.
+
+        * inspector/unit-tests/multimap.html: Added.
+        * inspector/unit-tests/multimap-expected.txt: Added.
+
 2019-03-20  Ryan Haddad  <ryanhaddad@apple.com>
 
         Unreviewed, rebaseline two tests after r243241.
diff --git a/LayoutTests/inspector/unit-tests/multimap-expected.txt b/LayoutTests/inspector/unit-tests/multimap-expected.txt
new file mode 100644 (file)
index 0000000..9a5c1d9
--- /dev/null
@@ -0,0 +1,81 @@
+Testing all methods of Multimap.
+
+
+== Running test suite: Multimap
+-- Running test case: Multimap.prototype.constructor.Empty
+[]
+
+-- Running test case: Multimap.prototype.constructor.NonEmpty
+[["zero","one"],["zero","four"],["two","three"]]
+
+-- Running test case: Multimap.prototype.has
+PASS: has should return true if a key exists.
+PASS: has should return true if a value exists for an existing key.
+PASS: has should return false if a value doesn't exist for an existing key.
+PASS: has should return false if a key doesn't exist.
+PASS: has should return false if a key doesn't exist, even if the value exists for a different key.
+
+-- Running test case: Multimap.prototype.add.RepeatingKeysUniqueValues
+[["zero","one"],["zero","two"]]
+
+-- Running test case: Multimap.prototype.add.UniqueKeysRepeatingValues
+[["zero","one"],["two","one"],["three","one"]]
+
+-- Running test case: Multimap.prototype.add.RepeatingKeysRepeatingValues
+[["zero","one"],["zero","three"],["two","one"]]
+
+-- Running test case: Multimap.prototype.delete
+[[0,1],[2,3],[2,4]]
+PASS: The key 0 and the value 1 were successfully deleted.
+[[2,3],[2,4]]
+PASS: The key 2 and the value 3 were successfully deleted.
+[[2,4]]
+PASS: The key 2 and the value 4 were successfully deleted.
+[]
+
+-- Running test case: Multimap.prototype.delete.NonExistingValues
+[[0,1],[2,3],[4,4]]
+PASS: Nothing was removed for key 0 and value 3.
+PASS: Nothing was removed for key 0 and value 4.
+PASS: Nothing was removed for key 2 and value 1.
+PASS: Nothing was removed for key 2 and value 4.
+PASS: Nothing was removed for key 4 and value 1.
+PASS: Nothing was removed for key 4 and value 3.
+[[0,1],[2,3],[4,4]]
+
+-- Running test case: Multimap.prototype.delete.NonExistingKeys
+[[0,1],[2,3],[4,4]]
+PASS: Nothing was removed for key 1.
+PASS: Nothing was removed for key 3.
+PASS: Nothing was removed for key 5.
+[[0,1],[2,3],[4,4]]
+
+-- Running test case: Multimap.prototype.delete.AllValuesForKey
+[["opossum","badger"],["opossum","raccoon"],["raccoon","opossum"]]
+PASS: Nothing was removed for key "badger".
+[["opossum","badger"],["opossum","raccoon"],["raccoon","opossum"]]
+PASS: Values were removed for key "opossum".
+[["raccoon","opossum"]]
+
+-- Running test case: Multimap.prototype.clear
+[["one","two"],["one","five"],["three","four"],["three","six"]]
+[]
+
+-- Running test case: Multimap.prototype.keys
+[["one","two"],["one","five"],["three","four"],["three","six"]]
+["one","three"]
+
+-- Running test case: Multimap.prototype.values
+[["one","two"],["one","five"],["three","four"],["three","six"]]
+["two","five","four","six"]
+
+-- Running test case: Multimap.prototype.sets
+[["one","two"],["one","five"],["three","four"],["three","six"]]
+[["one",["two","five"]],["three",["four","six"]]]
+
+-- Running test case: Multimap.prototype.copy
+[["one","two"],["three","four"]]
+PASS: Copy should not return the same object.
+PASS: Copy should return a deep copy.
+PASS: Modifying the original should not modify the copy.
+
diff --git a/LayoutTests/inspector/unit-tests/multimap.html b/LayoutTests/inspector/unit-tests/multimap.html
new file mode 100644 (file)
index 0000000..d473a80
--- /dev/null
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createSyncSuite("Multimap");
+
+    suite.addTestCase({
+        name: "Multimap.prototype.constructor.Empty",
+        test() {
+            let multimap = new Multimap;
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.constructor.NonEmpty",
+        test() {
+            let multimap = new Multimap([["zero", "one"], ["two", "three"], ["zero", "four"]]);
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.has",
+        test() {
+            let multimap = new Multimap([["zero", "one"], ["two", "three"], ["zero", "four"]]);
+
+            InspectorTest.expectThat(multimap.has("zero"), "has should return true if a key exists.");
+            InspectorTest.expectThat(multimap.has("zero", "one"), "has should return true if a value exists for an existing key.");
+            InspectorTest.expectFalse(multimap.has("zero", "three"), "has should return false if a value doesn't exist for an existing key.");
+            InspectorTest.expectFalse(multimap.has("one"), "has should return false if a key doesn't exist.");
+            InspectorTest.expectFalse(multimap.has("one", "three"), "has should return false if a key doesn't exist, even if the value exists for a different key.");
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.add.RepeatingKeysUniqueValues",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("zero", "one");
+            multimap.add("zero", "two");
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.add.UniqueKeysRepeatingValues",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("zero", "one");
+            multimap.add("two", "one");
+            multimap.add("three", "one");
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.add.RepeatingKeysRepeatingValues",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("zero", "one");
+            multimap.add("two", "one");
+            multimap.add("zero", "one");
+            multimap.add("zero", "three");
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.delete",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add(0, 1);
+            multimap.add(2, 3);
+            multimap.add(2, 4);
+
+            InspectorTest.log(multimap);
+
+            InspectorTest.expectThat(multimap.delete(0, 1), "The key 0 and the value 1 were successfully deleted.");
+
+            InspectorTest.log(multimap);
+
+            InspectorTest.expectThat(multimap.delete(2, 3), "The key 2 and the value 3 were successfully deleted.");
+
+            InspectorTest.log(multimap);
+
+            InspectorTest.expectThat(multimap.delete(2, 4), "The key 2 and the value 4 were successfully deleted.");
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.delete.NonExistingValues",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add(0, 1);
+            multimap.add(2, 3);
+            multimap.add(4, 4);
+
+            InspectorTest.log(multimap);
+
+            InspectorTest.expectFalse(multimap.delete(0, 3), "Nothing was removed for key 0 and value 3.");
+            InspectorTest.expectFalse(multimap.delete(0, 4), "Nothing was removed for key 0 and value 4.");
+            InspectorTest.expectFalse(multimap.delete(2, 1), "Nothing was removed for key 2 and value 1.");
+            InspectorTest.expectFalse(multimap.delete(2, 4), "Nothing was removed for key 2 and value 4.");
+            InspectorTest.expectFalse(multimap.delete(4, 1), "Nothing was removed for key 4 and value 1.");
+            InspectorTest.expectFalse(multimap.delete(4, 3), "Nothing was removed for key 4 and value 3.");
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.delete.NonExistingKeys",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add(0, 1);
+            multimap.add(2, 3);
+            multimap.add(4, 4);
+
+            InspectorTest.log(multimap);
+
+            InspectorTest.expectFalse(multimap.delete(1), "Nothing was removed for key 1.");
+            InspectorTest.expectFalse(multimap.delete(3), "Nothing was removed for key 3.");
+            InspectorTest.expectFalse(multimap.delete(5), "Nothing was removed for key 5.");
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.delete.AllValuesForKey",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("opossum", "badger");
+            multimap.add("opossum", "raccoon");
+            multimap.add("raccoon", "opossum");
+
+            InspectorTest.log(multimap);
+
+            InspectorTest.expectFalse(multimap.delete("badger"), `Nothing was removed for key "badger".`);
+
+            InspectorTest.log(multimap);
+
+            InspectorTest.expectThat(multimap.delete("opossum"), `Values were removed for key "opossum".`);
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.clear",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("one", "two");
+            multimap.add("three", "four");
+            multimap.add("one", "five");
+            multimap.add("three", "six");
+
+            InspectorTest.log(multimap);
+
+            multimap.clear();
+
+            InspectorTest.log(multimap);
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.keys",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("one", "two");
+            multimap.add("three", "four");
+            multimap.add("one", "five");
+            multimap.add("three", "six");
+
+            InspectorTest.log(multimap);
+            InspectorTest.log(Array.from(multimap.keys()));
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.values",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("one", "two");
+            multimap.add("three", "four");
+            multimap.add("one", "five");
+            multimap.add("three", "six");
+
+            InspectorTest.log(multimap);
+            InspectorTest.log(Array.from(multimap.values()));
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.sets",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("one", "two");
+            multimap.add("three", "four");
+            multimap.add("one", "five");
+            multimap.add("three", "six");
+
+            InspectorTest.log(multimap);
+            InspectorTest.log(Array.from(multimap.sets()).map(([key, value]) => [key, Array.from(value)]));
+        },
+    });
+
+    suite.addTestCase({
+        name: "Multimap.prototype.copy",
+        test() {
+            let multimap = new Multimap;
+
+            multimap.add("one", "two");
+            multimap.add("three", "four");
+
+            InspectorTest.log(multimap);
+
+            let copy = multimap.copy();
+
+            InspectorTest.expectNotEqual(multimap, copy, "Copy should not return the same object.")
+            InspectorTest.expectEqual(JSON.stringify(multimap), JSON.stringify(copy), "Copy should return a deep copy.");
+
+            multimap.add("five", "six");
+
+            InspectorTest.expectNotShallowEqual(multimap, copy, "Modifying the original should not modify the copy.");
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing all methods of Multimap.</p>
+</body>
+</html>
index cf18fff..0fb96d0 100644 (file)
@@ -1,5 +1,61 @@
 2019-03-20  Devin Rousso  <drousso@apple.com>
 
+        Web Inspector: Styles Redesign: Editing selector should not hide the rule
+        https://bugs.webkit.org/show_bug.cgi?id=178489
+        <rdar://problem/35062434>
+
+        Reviewed by Timothy Hatcher.
+
+        Extracts the selector payload parsing logic inside `WI.DOMNodeStyles` into static functions
+        so that when the user changes the selector of a `WI.CSSRule`, it's able to process and
+        update itself with the new selector. This is mainly useful in the case where the `WI.CSSRule`
+        no longer applies to the selected node (meaning it won't be part of that node's
+        `WI.DOMNodeStyles`) in that it allows the `WI.SpreadsheetCSSStyleDeclarationSection` to
+        display the new selector text and the owner `WI.SpreadsheetRulesStyleDetailsPanel` to keep
+        that section visible even though it isn't applicable to the current node anymore.
+
+        * UserInterface/Models/DOMNodeStyles.js:
+        (WI.DOMNodeStyles):
+        (WI.DOMNodeStyles.parseSelectorListPayload): Added.
+        (WI.DOMNodeStyles.createSourceCodeLocation): Added.
+        (WI.DOMNodeStyles.prototype.refresh):
+        (WI.DOMNodeStyles.prototype.refresh.fetchedMatchedStyles):
+        (WI.DOMNodeStyles.prototype.refresh.fetchedInlineStyles):
+        (WI.DOMNodeStyles.prototype.refresh.fetchedComputedStyle):
+        (WI.DOMNodeStyles.prototype._parseStyleDeclarationPayload):
+        (WI.DOMNodeStyles.prototype._parseRulePayload):
+        (WI.DOMNodeStyles.prototype._styleSheetContentDidChange):
+        (WI.DOMNodeStyles.prototype.refresh.parseRuleMatchArrayPayload): Deleted.
+        (WI.DOMNodeStyles.prototype._createSourceCodeLocation): Deleted.
+        (WI.DOMNodeStyles.prototype._parseSelectorListPayload): Deleted.
+        Keep track of all `WI.CSSRule` and `WI.CSSStyleDeclaration` that have ever been associated
+        with this object, so that if a rule's selector is changed to no longer match, and then is
+        changed back to match again, we are able to update that rule instead of creating a new one.
+
+        * UserInterface/Views/SpreadsheetRulesStyleDetailsPanel.js:
+        (WI.SpreadsheetRulesStyleDetailsPanel.prototype.layout):
+        (WI.SpreadsheetRulesStyleDetailsPanel.prototype._handleSectionFilterApplied):
+        (WI.SpreadsheetRulesStyleDetailsPanel.prototype._handleSectionSelectorWillChange): Added.
+        Attempt to preserve the position of any sections that are changed and no longer apply to the
+        current node.
+
+        * UserInterface/Views/SpreadsheetCSSStyleDeclarationSection.js:
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype.spreadsheetSelectorFieldDidChange):
+        (WI.SpreadsheetCSSStyleDeclarationSection.prototype._renderSelector):
+        Drive-by: remove unused CSS classes.
+
+        * UserInterface/Models/CSSRule.js:
+        (WI.CSSRule.prototype.update):
+        (WI.CSSRule.prototype._selectorResolved):
+        Drive-by: remove unused event.
+
+        * UserInterface/Base/Multimap.js:
+        (Multimap.prototype.has): Added.
+        (Multimap.prototype.sets): Added.
+        (Multimap.prototype.copy): Added.
+
+2019-03-20  Devin Rousso  <drousso@apple.com>
+
         Web Inspector: no way to filter out all console messages or all evaluations/results
         https://bugs.webkit.org/show_bug.cgi?id=167035
         <rdar://problem/30023523>
index f03aead..500eac0 100644 (file)
@@ -35,6 +35,14 @@ class Multimap
 
     // Public
 
+    has(key, value)
+    {
+        let valueSet = this._map.get(key);
+        if (!valueSet)
+            return false;
+        return value === undefined || valueSet.has(value);
+    }
+
     get(key)
     {
         return this._map.get(key);
@@ -88,6 +96,11 @@ class Multimap
         }
     }
 
+    sets()
+    {
+        return this._map.entries();
+    }
+
     *[Symbol.iterator]()
     {
         for (let [key, valueSet] of this._map) {
@@ -96,6 +109,11 @@ class Multimap
         }
     }
 
+    copy()
+    {
+        return new Multimap(this.toJSON());
+    }
+
     toJSON()
     {
         return Array.from(this);
index 4562122..8d8e259 100644 (file)
@@ -55,7 +55,7 @@ WI.CSSRule = class CSSRule extends WI.Object
         return !!this._id && (this._type === WI.CSSStyleSheet.Type.Author || this._type === WI.CSSStyleSheet.Type.Inspector);
     }
 
-    update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList, dontFireEvents)
+    update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList)
     {
         sourceCodeLocation = sourceCodeLocation || null;
         selectorText = selectorText || "";
@@ -64,14 +64,6 @@ WI.CSSRule = class CSSRule extends WI.Object
         style = style || null;
         mediaList = mediaList || [];
 
-        var changed = false;
-        if (!dontFireEvents) {
-            changed = this._selectorText !== selectorText || !Array.shallowEqual(this._selectors, selectors) ||
-                !Array.shallowEqual(this._matchedSelectorIndices, matchedSelectorIndices) || this._style !== style ||
-                !!this._sourceCodeLocation !== !!sourceCodeLocation || this._mediaList.length !== mediaList.length;
-            // FIXME: Look for differences in the media list arrays.
-        }
-
         if (this._style)
             this._style.ownerRule = null;
 
@@ -84,9 +76,6 @@ WI.CSSRule = class CSSRule extends WI.Object
 
         if (this._style)
             this._style.ownerRule = this;
-
-        if (changed)
-            this.dispatchEventToListeners(WI.CSSRule.Event.Changed);
     }
 
     get type()
@@ -162,11 +151,36 @@ WI.CSSRule = class CSSRule extends WI.Object
 
     _selectorResolved(rulePayload)
     {
+        if (rulePayload) {
+            let selectorText = rulePayload.selectorList.text;
+            if (selectorText !== this._selectorText) {
+                let selectors = WI.DOMNodeStyles.parseSelectorListPayload(rulePayload.selectorList);
+
+                let sourceCodeLocation = null;
+                let sourceRange = rulePayload.selectorList.range;
+                if (sourceRange) {
+                    sourceCodeLocation = WI.DOMNodeStyles.createSourceCodeLocation(rulePayload.sourceURL, {
+                        line: sourceRange.startLine,
+                        column: sourceRange.startColumn,
+                        documentNode: this._nodeStyles.node.ownerDocument,
+                    });
+                }
+
+                if (this._ownerStyleSheet) {
+                    if (!sourceCodeLocation && this._ownerStyleSheet.isInspectorStyleSheet())
+                        sourceCodeLocation = this._ownerStyleSheet.createSourceCodeLocation(sourceRange.startLine, sourceRange.startColumn);
+
+                    sourceCodeLocation = this._ownerStyleSheet.offsetSourceCodeLocation(sourceCodeLocation);
+                }
+
+                this.update(sourceCodeLocation, selectorText, selectors, [], this._style, this._mediaList);
+            }
+        }
+
         this.dispatchEventToListeners(WI.CSSRule.Event.SelectorChanged, {valid: !!rulePayload});
     }
 };
 
 WI.CSSRule.Event = {
-    Changed: "css-rule-changed",
     SelectorChanged: "css-rule-invalid-selector"
 };
index 5135589..a57b520 100644 (file)
@@ -32,8 +32,8 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
         console.assert(node);
         this._node = node || null;
 
-        this._rulesMap = {};
-        this._styleDeclarationsMap = {};
+        this._rulesMap = new Map;
+        this._stylesMap = new Multimap;
 
         this._matchedRules = [];
         this._inheritedRules = [];
@@ -51,6 +51,51 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
 
     // Static
 
+    static parseSelectorListPayload(selectorList)
+    {
+        let selectors = selectorList.selectors;
+        if (!selectors.length)
+            return [];
+
+        // COMPATIBILITY (iOS 8): The selectorList payload was an array of selector text strings.
+        // Now they are CSSSelector objects with multiple properties.
+        if (typeof selectors[0] === "string") {
+            return selectors.map(function(selectorText) {
+                return new WI.CSSSelector(selectorText);
+            });
+        }
+
+        return selectors.map(function(selectorPayload) {
+            return new WI.CSSSelector(selectorPayload.text, selectorPayload.specificity, selectorPayload.dynamic);
+        });
+    }
+
+    static createSourceCodeLocation(sourceURL, {line, column, documentNode} = {})
+    {
+        if (!sourceURL)
+            return null;
+
+        let sourceCode = null;
+
+        // Try to use the node to find the frame which has the correct resource first.
+        if (documentNode) {
+            let mainResource = WI.networkManager.resourceForURL(documentNode.documentURL);
+            if (mainResource) {
+                let parentFrame = mainResource.parentFrame;
+                sourceCode = parentFrame.resourceForURL(sourceURL);
+            }
+        }
+
+        // If that didn't find the resource, then search all frames.
+        if (!sourceCode)
+            sourceCode = WI.networkManager.resourceForURL(sourceURL);
+
+        if (!sourceCode)
+            return null;
+
+        return sourceCode.createSourceCodeLocation(line || 0, column || 0);
+    }
+
     static uniqueOrderedStyles(orderedStyles)
     {
         let uniqueOrderedStyles = [];
@@ -104,6 +149,8 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
 
         this._needsRefresh = false;
 
+        let previousStylesMap = this._stylesMap.copy();
+
         let fetchedMatchedStylesPromise = new WI.WrappedPromise;
         let fetchedInlineStylesPromise = new WI.WrappedPromise;
         let fetchedComputedStylesPromise = new WI.WrappedPromise;
@@ -120,21 +167,20 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
             };
         }
 
-        function parseRuleMatchArrayPayload(matchArray, node, inherited)
-        {
+        let parseRuleMatchArrayPayload = (matchArray, node, inherited, pseudoId) => {
             var result = [];
 
             // Iterate in reverse order to match the cascade order.
             var ruleOccurrences = {};
             for (var i = matchArray.length - 1; i >= 0; --i) {
-                var rule = this._parseRulePayload(matchArray[i].rule, matchArray[i].matchingSelectors, node, inherited, ruleOccurrences);
+                var rule = this._parseRulePayload(matchArray[i].rule, matchArray[i].matchingSelectors, node, inherited, pseudoId, ruleOccurrences);
                 if (!rule)
                     continue;
                 result.push(rule);
             }
 
             return result;
-        }
+        };
 
         function fetchedMatchedStyles(error, matchedRulesPayload, pseudoElementRulesPayload, inheritedRulesPayload)
         {
@@ -142,20 +188,12 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
             pseudoElementRulesPayload = pseudoElementRulesPayload || [];
             inheritedRulesPayload = inheritedRulesPayload || [];
 
-            // Move the current maps to previous.
-            this._previousRulesMap = this._rulesMap;
-            this._previousStyleDeclarationsMap = this._styleDeclarationsMap;
-
-            // Clear the current maps.
-            this._rulesMap = {};
-            this._styleDeclarationsMap = {};
-
-            this._matchedRules = parseRuleMatchArrayPayload.call(this, matchedRulesPayload, this._node);
+            this._matchedRules = parseRuleMatchArrayPayload(matchedRulesPayload, this._node);
 
             this._pseudoElements.clear();
-            for (var pseudoElementRulePayload of pseudoElementRulesPayload) {
-                var pseudoElementRules = parseRuleMatchArrayPayload.call(this, pseudoElementRulePayload.matches, this._node);
-                this._pseudoElements.set(pseudoElementRulePayload.pseudoId, {matchedRules: pseudoElementRules});
+            for (let {pseudoId, matches} of pseudoElementRulesPayload) {
+                let pseudoElementRules = parseRuleMatchArrayPayload(matches, this._node, false, pseudoId);
+                this._pseudoElements.set(pseudoId, {matchedRules: pseudoElementRules});
             }
 
             this._inheritedRules = [];
@@ -166,8 +204,8 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
                 var inheritedRulePayload = inheritedRulesPayload[i];
 
                 var inheritedRuleInfo = {node: currentNode};
-                inheritedRuleInfo.inlineStyle = inheritedRulePayload.inlineStyle ? this._parseStyleDeclarationPayload(inheritedRulePayload.inlineStyle, currentNode, true, WI.CSSStyleDeclaration.Type.Inline) : null;
-                inheritedRuleInfo.matchedRules = inheritedRulePayload.matchedCSSRules ? parseRuleMatchArrayPayload.call(this, inheritedRulePayload.matchedCSSRules, currentNode, true) : [];
+                inheritedRuleInfo.inlineStyle = inheritedRulePayload.inlineStyle ? this._parseStyleDeclarationPayload(inheritedRulePayload.inlineStyle, currentNode, true, null, WI.CSSStyleDeclaration.Type.Inline) : null;
+                inheritedRuleInfo.matchedRules = inheritedRulePayload.matchedCSSRules ? parseRuleMatchArrayPayload(inheritedRulePayload.matchedCSSRules, currentNode, true) : [];
 
                 if (inheritedRuleInfo.inlineStyle || inheritedRuleInfo.matchedRules.length)
                     this._inheritedRules.push(inheritedRuleInfo);
@@ -181,8 +219,8 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
 
         function fetchedInlineStyles(error, inlineStylePayload, attributesStylePayload)
         {
-            this._inlineStyle = inlineStylePayload ? this._parseStyleDeclarationPayload(inlineStylePayload, this._node, false, WI.CSSStyleDeclaration.Type.Inline) : null;
-            this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, WI.CSSStyleDeclaration.Type.Attribute) : null;
+            this._inlineStyle = inlineStylePayload ? this._parseStyleDeclarationPayload(inlineStylePayload, this._node, false, null, WI.CSSStyleDeclaration.Type.Inline) : null;
+            this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, null, WI.CSSStyleDeclaration.Type.Attribute) : null;
 
             this._updateStyleCascade();
 
@@ -210,18 +248,15 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
                 this._computedStyle = new WI.CSSStyleDeclaration(this, null, null, WI.CSSStyleDeclaration.Type.Computed, this._node, false, null, properties);
 
             let significantChange = false;
-            for (let key in this._styleDeclarationsMap) {
-                // Check if the same key exists in the previous map and has the same style objects.
-                if (key in this._previousStyleDeclarationsMap) {
-                    if (Array.shallowEqual(this._styleDeclarationsMap[key], this._previousStyleDeclarationsMap[key]))
-                        continue;
-
+            for (let [key, styles] of this._stylesMap.sets()) {
+                let previousStyles = previousStylesMap.get(key);
+                if (previousStyles) {
                     // Some styles have selectors such that they will match with the DOM node twice (for example "::before, ::after").
                     // In this case a second style for a second matching may be generated and added which will cause the shallowEqual
                     // to not return true, so in this case we just want to ensure that all the current styles existed previously.
                     let styleFound = false;
-                    for (let style of this._styleDeclarationsMap[key]) {
-                        if (this._previousStyleDeclarationsMap[key].includes(style)) {
+                    for (let style of styles) {
+                        if (previousStyles.has(style)) {
                             styleFound = true;
                             break;
                         }
@@ -233,7 +268,7 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
 
                 if (!this._includeUserAgentRulesOnNextRefresh) {
                     // We can assume all the styles with the same key are from the same stylesheet and rule, so we only check the first.
-                    let firstStyle = this._styleDeclarationsMap[key][0];
+                    let firstStyle = styles.firstValue;
                     if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WI.CSSStyleSheet.Type.UserAgent) {
                         // User Agent styles get different identifiers after some edits. This would cause us to fire a significant refreshed
                         // event more than it is helpful. And since the user agent stylesheet is static it shouldn't match differently
@@ -248,14 +283,14 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
             }
 
             if (!significantChange) {
-                for (var key in this._previousStyleDeclarationsMap) {
+                for (let [key, previousStyles] of previousStylesMap.sets()) {
                     // Check if the same key exists in current map. If it does exist it was already checked for equality above.
-                    if (key in this._styleDeclarationsMap)
+                    if (this._stylesMap.has(key))
                         continue;
 
                     if (!this._includeUserAgentRulesOnNextRefresh) {
                         // See above for why we skip user agent style rules.
-                        var firstStyle = this._previousStyleDeclarationsMap[key][0];
+                        let firstStyle = previousStyles.firstValue;
                         if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WI.CSSStyleSheet.Type.UserAgent)
                             continue;
                     }
@@ -266,11 +301,7 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
                 }
             }
 
-            delete this._includeUserAgentRulesOnNextRefresh;
-
-            // Delete the previous maps now that any reused rules and style have been moved over.
-            delete this._previousRulesMap;
-            delete this._previousStyleDeclarationsMap;
+            this._includeUserAgentRulesOnNextRefresh = false
 
             this.dispatchEventToListeners(WI.DOMNodeStyles.Event.Refreshed, {significantChange});
 
@@ -480,32 +511,6 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
 
     // Private
 
-    _createSourceCodeLocation(sourceURL, sourceLine, sourceColumn)
-    {
-        if (!sourceURL)
-            return null;
-
-        var sourceCode;
-
-        // Try to use the node to find the frame which has the correct resource first.
-        if (this._node.ownerDocument) {
-            var mainResource = WI.networkManager.resourceForURL(this._node.ownerDocument.documentURL);
-            if (mainResource) {
-                var parentFrame = mainResource.parentFrame;
-                sourceCode = parentFrame.resourceForURL(sourceURL);
-            }
-        }
-
-        // If that didn't find the resource, then search all frames.
-        if (!sourceCode)
-            sourceCode = WI.networkManager.resourceForURL(sourceURL);
-
-        if (!sourceCode)
-            return null;
-
-        return sourceCode.createSourceCodeLocation(sourceLine || 0, sourceColumn || 0);
-    }
-
     _parseSourceRangePayload(payload)
     {
         if (!payload)
@@ -590,7 +595,7 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
         return new WI.CSSProperty(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
     }
 
-    _parseStyleDeclarationPayload(payload, node, inherited, type, rule, updateAllStyles)
+    _parseStyleDeclarationPayload(payload, node, inherited, pseudoId, type, rule)
     {
         if (!payload)
             return null;
@@ -600,60 +605,25 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
 
         var id = payload.styleId;
         var mapKey = id ? id.styleSheetId + ":" + id.ordinal : null;
-
+        if (pseudoId)
+            mapKey += ":" + pseudoId;
         if (type === WI.CSSStyleDeclaration.Type.Attribute)
-            mapKey = node.id + ":attribute";
-
-        var styleDeclaration = rule ? rule.style : null;
-        var styleDeclarations = [];
-
-        // Look for existing styles in the previous map if there is one, otherwise use the current map.
-        var previousStyleDeclarationsMap = this._previousStyleDeclarationsMap || this._styleDeclarationsMap;
-        if (mapKey && mapKey in previousStyleDeclarationsMap) {
-            styleDeclarations = previousStyleDeclarationsMap[mapKey];
-
-            // If we need to update all styles, then stop here and call _parseStyleDeclarationPayload for each style.
-            // We need to parse multiple times so we reuse the right properties from each style.
-            if (updateAllStyles && styleDeclarations.length) {
-                for (var i = 0; i < styleDeclarations.length; ++i) {
-                    var styleDeclaration = styleDeclarations[i];
-                    this._parseStyleDeclarationPayload(payload, styleDeclaration.node, styleDeclaration.inherited, styleDeclaration.type, styleDeclaration.ownerRule);
-                }
-
-                return null;
-            }
-
-            if (!styleDeclaration) {
-                var filteredStyleDeclarations = styleDeclarations.filter(function(styleDeclaration) {
-                    // This case only applies for styles that are not part of a rule.
-                    if (styleDeclaration.ownerRule) {
-                        console.assert(!rule);
-                        return false;
-                    }
-
-                    if (styleDeclaration.node !== node)
-                        return false;
-
-                    if (styleDeclaration.inherited !== inherited)
-                        return false;
+            mapKey += ":" + node.id + ":attribute";
 
-                    return true;
-                });
+        let style = rule ? rule.style : null;
 
-                console.assert(filteredStyleDeclarations.length <= 1);
-                styleDeclaration = filteredStyleDeclarations[0] || null;
+        let existingStyles = this._stylesMap.get(mapKey);
+        if (existingStyles && !style) {
+            for (let existingStyle of existingStyles) {
+                if (existingStyle.node === node && existingStyle.inherited === inherited) {
+                    style = existingStyle;
+                    break;
+                }
             }
         }
 
-        if (previousStyleDeclarationsMap !== this._styleDeclarationsMap) {
-            // If the previous and current maps differ then make sure the found styleDeclaration is added to the current map.
-            styleDeclarations = mapKey && mapKey in this._styleDeclarationsMap ? this._styleDeclarationsMap[mapKey] : [];
-
-            if (styleDeclaration && !styleDeclarations.includes(styleDeclaration)) {
-                styleDeclarations.push(styleDeclaration);
-                this._styleDeclarationsMap[mapKey] = styleDeclarations;
-            }
-        }
+        if (style)
+            this._stylesMap.add(mapKey, style);
 
         var shorthands = {};
         for (var i = 0; payload.shorthandEntries && i < payload.shorthandEntries.length; ++i) {
@@ -670,16 +640,16 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
             if (inherited && WI.CSSProperty.isInheritedPropertyName(propertyPayload.name))
                 ++inheritedPropertyCount;
 
-            let property = this._parseStylePropertyPayload(propertyPayload, i, styleDeclaration);
+            let property = this._parseStylePropertyPayload(propertyPayload, i, style);
             properties.push(property);
         }
 
         let text = payload.cssText;
         var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
 
-        if (styleDeclaration) {
-            styleDeclaration.update(text, properties, styleSheetTextRange);
-            return styleDeclaration;
+        if (style) {
+            style.update(text, properties, styleSheetTextRange);
+            return style;
         }
 
         var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
@@ -692,36 +662,15 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
         if (inherited && !inheritedPropertyCount)
             return null;
 
-        styleDeclaration = new WI.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange);
+        style = new WI.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange);
 
-        if (mapKey) {
-            styleDeclarations.push(styleDeclaration);
-            this._styleDeclarationsMap[mapKey] = styleDeclarations;
-        }
-
-        return styleDeclaration;
-    }
-
-    _parseSelectorListPayload(selectorList)
-    {
-        var selectors = selectorList.selectors;
-        if (!selectors.length)
-            return [];
-
-        // COMPATIBILITY (iOS 8): The selectorList payload was an array of selector text strings.
-        // Now they are CSSSelector objects with multiple properties.
-        if (typeof selectors[0] === "string") {
-            return selectors.map(function(selectorText) {
-                return new WI.CSSSelector(selectorText);
-            });
-        }
+        if (mapKey)
+            this._stylesMap.add(mapKey, style);
 
-        return selectors.map(function(selectorPayload) {
-            return new WI.CSSSelector(selectorPayload.text, selectorPayload.specificity, selectorPayload.dynamic);
-        });
+        return style;
     }
 
-    _parseRulePayload(payload, matchedSelectorIndices, node, inherited, ruleOccurrences)
+    _parseRulePayload(payload, matchedSelectorIndices, node, inherited, pseudoId, ruleOccurrences)
     {
         if (!payload)
             return null;
@@ -732,7 +681,7 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
         // editability solely based on the existence of the id like the open source front-end does.
         var id = payload.ruleId || payload.style.styleId;
 
-        var mapKey = id ? id.styleSheetId + ":" + id.ordinal + ":" + (inherited ? "I" : "N") + ":" + node.id : null;
+        var mapKey = id ? id.styleSheetId + ":" + id.ordinal + ":" + (inherited ? "I" : "N") + ":" + (pseudoId ? pseudoId + ":" : "") + node.id : null;
 
         // Rules can match multiple times if they have multiple selectors or because of inheritance. We keep a count
         // of occurrences so we have unique rules per occurrence, that way properties will be correctly marked as overridden.
@@ -747,36 +696,32 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
             mapKey += ":" + occurrence;
         }
 
-        var rule = null;
+        let rule = this._rulesMap.get(mapKey);
 
-        // Look for existing rules in the previous map if there is one, otherwise use the current map.
-        var previousRulesMap = this._previousRulesMap || this._rulesMap;
-        if (mapKey && mapKey in previousRulesMap) {
-            rule = previousRulesMap[mapKey];
-
-            if (previousRulesMap !== this._rulesMap) {
-                // If the previous and current maps differ then make sure the found rule is added to the current map.
-                this._rulesMap[mapKey] = rule;
-            }
-        }
-
-        var style = this._parseStyleDeclarationPayload(payload.style, node, inherited, WI.CSSStyleDeclaration.Type.Rule, rule);
+        var style = this._parseStyleDeclarationPayload(payload.style, node, inherited, pseudoId, WI.CSSStyleDeclaration.Type.Rule, rule);
         if (!style)
             return null;
 
         var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
 
         var selectorText = payload.selectorList.text;
-        var selectors = this._parseSelectorListPayload(payload.selectorList);
+        let selectors = DOMNodeStyles.parseSelectorListPayload(payload.selectorList);
         var type = WI.CSSManager.protocolStyleSheetOriginToEnum(payload.origin);
 
         var sourceCodeLocation = null;
         var sourceRange = payload.selectorList.range;
-        if (sourceRange)
-            sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, sourceRange.startLine, sourceRange.startColumn);
-        else {
+        if (sourceRange) {
+            sourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(payload.sourceURL, {
+                line: sourceRange.startLine,
+                column: sourceRange.startColumn,
+                documentNode: this._node.ownerDocument,
+            });
+        } else {
             // FIXME: Is it possible for a CSSRule to have a sourceLine without its selectorList having a sourceRange? Fall back just in case.
-            sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, payload.sourceLine);
+            sourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(payload.sourceURL, {
+                line: payload.sourceLine,
+                documentNode: this._node.ownerDocument,
+            });
         }
 
         if (styleSheet) {
@@ -791,7 +736,7 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
             var mediaItem = payload.media[i];
             var mediaType = WI.CSSManager.protocolMediaSourceToEnum(mediaItem.source);
             var mediaText = mediaItem.text;
-            var mediaSourceCodeLocation = this._createSourceCodeLocation(mediaItem.sourceURL, mediaItem.sourceLine);
+            let mediaSourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(mediaItem.sourceURL, {line: mediaItem.sourceLine});
             if (styleSheet)
                 mediaSourceCodeLocation = styleSheet.offsetSourceCodeLocation(mediaSourceCodeLocation);
 
@@ -809,7 +754,7 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
         rule = new WI.CSSRule(this, styleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList);
 
         if (mapKey)
-            this._rulesMap[mapKey] = rule;
+            this._rulesMap.set(mapKey, rule);
 
         return rule;
     }
@@ -829,7 +774,7 @@ WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
 
         // Ignore the stylesheet we know we just changed and handled above.
         if (styleSheet === this._ignoreNextContentDidChangeForStyleSheet) {
-            delete this._ignoreNextContentDidChangeForStyleSheet;
+            this._ignoreNextContentDidChangeForStyleSheet = null;
             return;
         }
 
index 59728ee..67f6a77 100644 (file)
@@ -174,6 +174,7 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
         if (!selectorText || selectorText === this._style.ownerRule.selectorText)
             this._discardSelectorChange();
         else {
+            this.dispatchEventToListeners(WI.SpreadsheetCSSStyleDeclarationSection.Event.SelectorWillChange);
             this._style.ownerRule.singleFireEventListener(WI.CSSRule.Event.SelectorChanged, this._renderSelector, this);
             this._style.ownerRule.selectorText = selectorText;
         }
@@ -302,18 +303,12 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
 
             var selectors = this._style.ownerRule.selectors;
             var matchedSelectorIndices = this._style.ownerRule.matchedSelectorIndices;
-            var alwaysMatch = !matchedSelectorIndices.length;
             if (selectors.length) {
-                let hasMatchingPseudoElementSelector = false;
                 for (let i = 0; i < selectors.length; ++i) {
-                    appendSelector(selectors[i], alwaysMatch || matchedSelectorIndices.includes(i));
+                    appendSelector(selectors[i], matchedSelectorIndices.includes(i));
                     if (i < selectors.length - 1)
                         this._selectorElement.append(", ");
-
-                    if (matchedSelectorIndices.includes(i) && selectors[i].isPseudoElementSelector())
-                        hasMatchingPseudoElementSelector = true;
                 }
-                this._element.classList.toggle("pseudo-element-selector", hasMatchingPseudoElementSelector);
             } else
                 appendSelectorTextKnownToMatch(this._style.ownerRule.selectorText);
 
@@ -544,6 +539,7 @@ WI.SpreadsheetCSSStyleDeclarationSection = class SpreadsheetCSSStyleDeclarationS
 
 WI.SpreadsheetCSSStyleDeclarationSection.Event = {
     FilterApplied: "spreadsheet-css-style-declaration-section-filter-applied",
+    SelectorWillChange: "spreadsheet-css-style-declaration-section-selector-will-change",
 };
 
 WI.SpreadsheetCSSStyleDeclarationSection.MatchedSelectorElementStyleClassName = "matched";
index 8f52af7..e1d4ebe 100644 (file)
@@ -214,57 +214,88 @@ WI.SpreadsheetRulesStyleDetailsPanel = class SpreadsheetRulesStyleDetailsPanel e
 
         this._shouldRefreshSubviews = false;
 
-        this.removeAllSubviews();
+        let oldSections = this._sections.slice();
+        let preservedSections = oldSections.filter((section) => {
+            if (section[SpreadsheetRulesStyleDetailsPanel.SectionShowingForNodeSymbol] !== this.nodeStyles.node) {
+                section[SpreadsheetRulesStyleDetailsPanel.SectionShowingForNodeSymbol] = null;
+                section[SpreadsheetRulesStyleDetailsPanel.SectionIndexSymbol] = -1;
+            }
+            return section[SpreadsheetRulesStyleDetailsPanel.SectionShowingForNodeSymbol];
+        });
+
+        if (preservedSections.length) {
+            for (let section of oldSections) {
+                if (!preservedSections.includes(section))
+                    this.removeSubview(section);
+            }
+            for (let header of this._headerMap.values())
+                header.remove();
+        } else
+            this.removeAllSubviews();
 
         let previousStyle = null;
+        let currentHeader = null;
         this._headerMap.clear();
         this._sections = [];
 
-        let createHeader = (text, nodeOrPseudoId) => {
-            let header = this.element.appendChild(document.createElement("h2"));
-            header.classList.add("section-header");
-            header.append(text);
+        let addHeader = (text, nodeOrPseudoId) => {
+            currentHeader = this.element.appendChild(document.createElement("h2"));
+            currentHeader.classList.add("section-header");
+            currentHeader.append(text);
 
             if (nodeOrPseudoId) {
                 if (nodeOrPseudoId instanceof WI.DOMNode) {
-                    header.append(" ", WI.linkifyNodeReference(nodeOrPseudoId, {
+                    currentHeader.append(" ", WI.linkifyNodeReference(nodeOrPseudoId, {
                         maxLength: 100,
                         excludeRevealElement: true,
                     }));
                 } else
-                    header.append(" ", WI.CSSManager.displayNameForPseudoId(nodeOrPseudoId));
+                    currentHeader.append(" ", WI.CSSManager.displayNameForPseudoId(nodeOrPseudoId));
+            }
+        };
+
+        let addSection = (section) => {
+            if (section.style.inherited && (!previousStyle || previousStyle.node !== section.style.node))
+                addHeader(WI.UIString("Inherited From"), section.style.node);
 
-                this._headerMap.set(nodeOrPseudoId, header);
+            if (!section.isDescendantOf(this)) {
+                let referenceView = this.subviews[this._sections.length];
+                if (!referenceView || referenceView[SpreadsheetRulesStyleDetailsPanel.SectionIndexSymbol] === this._sections.length)
+                    this.addSubview(section);
+                else
+                    this.insertSubviewBefore(section, referenceView);
             }
+
+            this._sections.push(section);
+            section.needsLayout();
+
+            if (currentHeader)
+                this._headerMap.set(section.style, currentHeader);
+
+            previousStyle = section.style;
         };
 
         let createSection = (style) => {
-            let section = style[WI.SpreadsheetRulesStyleDetailsPanel.RuleSection];
+            let section = style[SpreadsheetRulesStyleDetailsPanel.StyleSectionSymbol];
             if (!section) {
                 section = new WI.SpreadsheetCSSStyleDeclarationSection(this, style);
-                style[WI.SpreadsheetRulesStyleDetailsPanel.RuleSection] = section;
+                section.addEventListener(WI.SpreadsheetCSSStyleDeclarationSection.Event.FilterApplied, this._handleSectionFilterApplied, this);
+                section.addEventListener(WI.SpreadsheetCSSStyleDeclarationSection.Event.SelectorWillChange, this._handleSectionSelectorWillChange, this);
+                style[SpreadsheetRulesStyleDetailsPanel.StyleSectionSymbol] = section;
             }
 
-            section.addEventListener(WI.SpreadsheetCSSStyleDeclarationSection.Event.FilterApplied, this._handleSectionFilterApplied, this);
-
             if (this._newRuleSelector === style.selectorText && style.enabledProperties.length === 0)
                 section.startEditingRuleSelector();
 
-            this.addSubview(section);
-            section.needsLayout();
-            this._sections.push(section);
-
-            previousStyle = style;
+            addSection(section);
 
-            return section;
+            let preservedSection = preservedSections.find((sectionToPreserve) => sectionToPreserve[SpreadsheetRulesStyleDetailsPanel.SectionIndexSymbol] === this._sections.length - 1);
+            if (preservedSection)
+                addSection(preservedSection);
         };
 
-        for (let style of this.nodeStyles.uniqueOrderedStyles) {
-            if (style.inherited && (!previousStyle || previousStyle.node !== style.node))
-                createHeader(WI.UIString("Inherited From"), style.node);
-
+        for (let style of this.nodeStyles.uniqueOrderedStyles)
             createSection(style);
-        }
 
         let beforePseudoId = null;
         let afterPseudoId = null;
@@ -277,23 +308,17 @@ WI.SpreadsheetRulesStyleDetailsPanel = class SpreadsheetRulesStyleDetailsPanel e
             afterPseudoId = 5;
         }
 
+
         for (let [pseudoId, pseudoElementInfo] of this.nodeStyles.pseudoElements) {
-            let nodeOrPseudoId = null;
+            let pseudoElement = null;
             if (pseudoId === beforePseudoId)
-                nodeOrPseudoId = this.nodeStyles.node.beforePseudoElement();
+                pseudoElement = this.nodeStyles.node.beforePseudoElement();
             else if (pseudoId === afterPseudoId)
-                nodeOrPseudoId = this.nodeStyles.node.afterPseudoElement();
-            else
-                nodeOrPseudoId = pseudoId;
+                pseudoElement = this.nodeStyles.node.afterPseudoElement();
+            addHeader(WI.UIString("Pseudo-Element"), pseudoElement || pseudoId);
 
-            createHeader(WI.UIString("Pseudo-Element"), nodeOrPseudoId);
-
-            for (let style of WI.DOMNodeStyles.uniqueOrderedStyles(pseudoElementInfo.orderedStyles)) {
-                let section = createSection(style);
-
-                if (nodeOrPseudoId === pseudoId)
-                    section.__pseudoId = pseudoId;
-            }
+            for (let style of WI.DOMNodeStyles.uniqueOrderedStyles(pseudoElementInfo.orderedStyles))
+                createSection(style);
         }
 
         this._newRuleSelector = null;
@@ -320,7 +345,7 @@ WI.SpreadsheetRulesStyleDetailsPanel = class SpreadsheetRulesStyleDetailsPanel e
 
         this.element.classList.remove("filter-non-matching");
 
-        let header = this._headerMap.get(event.target.__pseudoId || event.target.style.node);
+        let header = this._headerMap.get(event.target.style);
         if (header)
             header.classList.remove(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName);
     }
@@ -335,6 +360,16 @@ WI.SpreadsheetRulesStyleDetailsPanel = class SpreadsheetRulesStyleDetailsPanel e
         const text = "";
         this.nodeStyles.addRule(this._newRuleSelector, text, stylesheetId);
     }
+
+    _handleSectionSelectorWillChange(event)
+    {
+        let section = event.target;
+        section[SpreadsheetRulesStyleDetailsPanel.SectionShowingForNodeSymbol] = this.nodeStyles.node;
+        section[SpreadsheetRulesStyleDetailsPanel.SectionIndexSymbol] = this._sections.indexOf(section);
+        console.assert(section[SpreadsheetRulesStyleDetailsPanel.SectionIndexSymbol] >= 0);
+    }
 };
 
-WI.SpreadsheetRulesStyleDetailsPanel.RuleSection = Symbol("rule-section");
+WI.SpreadsheetRulesStyleDetailsPanel.StyleSectionSymbol = Symbol("style-section");
+WI.SpreadsheetRulesStyleDetailsPanel.SectionShowingForNodeSymbol = Symbol("style-showing-for-node");
+WI.SpreadsheetRulesStyleDetailsPanel.SectionIndexSymbol = Symbol("style-index");