Web Inspector: Add Changes panel to Elements tab
authornvasilyev@apple.com <nvasilyev@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 28 Jan 2019 09:29:20 +0000 (09:29 +0000)
committernvasilyev@apple.com <nvasilyev@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 28 Jan 2019 09:29:20 +0000 (09:29 +0000)
https://bugs.webkit.org/show_bug.cgi?id=193803

Reviewed by Devin Rousso.

Source/WebInspectorUI:

Introduce the new experimental Changes Panel. It shows a list of CSS changes
made via Web Inspector, so the changes could be copied to the source files.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Base/Setting.js:
* UserInterface/Base/Utilities.js:
(Array.diffArrays): Added.

* UserInterface/Controllers/CSSManager.js:
(WI.CSSManager):
(WI.CSSManager.prototype.get modifiedCSSRules):
(WI.CSSManager.prototype.addModifiedCSSRule):
(WI.CSSManager.prototype.removeModifiedCSSRule):
(WI.CSSManager.prototype._mainResourceDidChange):

* UserInterface/Main.html:
* UserInterface/Models/CSSProperty.js:
(WI.CSSProperty):
(WI.CSSProperty.prototype.remove):
(WI.CSSProperty.prototype.replaceWithText):
(WI.CSSProperty.prototype.commentOut):
(WI.CSSProperty.prototype.set text):
(WI.CSSProperty.prototype.get modified):
(WI.CSSProperty.prototype.set name):
(WI.CSSProperty.prototype.set rawValue):
(WI.CSSProperty.prototype.get initialState):
(WI.CSSProperty.prototype._updateOwnerStyleText):
(WI.CSSProperty.prototype._markModified):
Mark CSSProperty modified *before* making any changes to copy its initial state.

* UserInterface/Models/CSSRule.js:
(WI.CSSRule):
(WI.CSSRule.prototype.get id):
(WI.CSSRule.prototype.get initialState):
(WI.CSSRule.prototype.get stringId):
(WI.CSSRule.prototype.markModified):

* UserInterface/Models/CSSStyleDeclaration.js:
(WI.CSSStyleDeclaration):
(WI.CSSStyleDeclaration.prototype.get initialState):
(WI.CSSStyleDeclaration.prototype.get enabledProperties):
(WI.CSSStyleDeclaration.prototype.get properties):
(WI.CSSStyleDeclaration.prototype.set properties):
(WI.CSSStyleDeclaration.prototype.propertyForName):
(WI.CSSStyleDeclaration.prototype.newBlankProperty):
(WI.CSSStyleDeclaration.prototype.markModified):

* UserInterface/Views/ChangesDetailsSidebarPanel.css: Added.
(.sidebar > .panel.changes-panel):
(.sidebar > .panel.changes-panel:not(.empty)):
(.sidebar > .panel.changes-panel.empty):
(.changes-panel ins):
(.changes-panel del):
(.changes-panel del.css-property::before):
(.changes-panel ins.css-property::before):
(@media (prefers-color-scheme: dark)):

* UserInterface/Views/ChangesDetailsSidebarPanel.js: Added.
(WI.ChangesDetailsSidebarPanel):
(WI.ChangesDetailsSidebarPanel.prototype.inspect):
(WI.ChangesDetailsSidebarPanel.prototype.supportsDOMNode):
(WI.ChangesDetailsSidebarPanel.prototype.shown):
(WI.ChangesDetailsSidebarPanel.prototype.detached):
(WI.ChangesDetailsSidebarPanel.prototype.layout):
(WI.ChangesDetailsSidebarPanel.prototype._mainResourceDidChange):

* UserInterface/Views/ElementsTabContentView.js:
(WI.ElementsTabContentView):

* UserInterface/Views/SettingsTabContentView.js:
(WI.SettingsTabContentView.prototype._createExperimentalSettingsView):

* UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css:
(.spreadsheet-style-declaration-editor .property):
(.spreadsheet-style-declaration-editor .property.modified):
(.spreadsheet-style-declaration-editor .property.modified:not(.selected)):
(@media (prefers-color-scheme: dark)):

* UserInterface/Views/SpreadsheetStyleProperty.js:
(WI.SpreadsheetStyleProperty.prototype.updateStatus):

LayoutTests:

Test newly added Array.diffArrays.

* inspector/unit-tests/array-utilities-expected.txt:
* inspector/unit-tests/array-utilities.html:
Use the old `InspectorTest.log` method since it shows diffs for actual and expected text.

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

18 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/array-utilities-expected.txt
LayoutTests/inspector/unit-tests/array-utilities.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Setting.js
Source/WebInspectorUI/UserInterface/Base/Utilities.js
Source/WebInspectorUI/UserInterface/Controllers/CSSManager.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/CSSProperty.js
Source/WebInspectorUI/UserInterface/Models/CSSRule.js
Source/WebInspectorUI/UserInterface/Models/CSSStyleDeclaration.js
Source/WebInspectorUI/UserInterface/Views/ChangesDetailsSidebarPanel.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ChangesDetailsSidebarPanel.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ElementsTabContentView.js
Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js
Source/WebInspectorUI/UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css
Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js

index 60ff27a..f499282 100644 (file)
@@ -1,3 +1,16 @@
+2019-01-28  Nikita Vasilyev  <nvasilyev@apple.com>
+
+        Web Inspector: Add Changes panel to Elements tab
+        https://bugs.webkit.org/show_bug.cgi?id=193803
+
+        Reviewed by Devin Rousso.
+
+        Test newly added Array.diffArrays.
+
+        * inspector/unit-tests/array-utilities-expected.txt:
+        * inspector/unit-tests/array-utilities.html:
+        Use the old `InspectorTest.log` method since it shows diffs for actual and expected text.
+
 2019-01-26  Simon Fraser  <simon.fraser@apple.com>
 
         Have composited RenderIFrame layers make FrameHosting scrolling tree nodes to parent the iframe's scrolling node
index 0b24225..e196911 100644 (file)
@@ -70,6 +70,20 @@ PASS: shallowEqual of a typed-array and non-array should be false.
 PASS: shallowEqual of a non-array with itself should be false.
 PASS: shallowEqual of non-arrays should be false.
 
+-- Running test case: Array.diffArrays
+["a"], [] => [["a",-1]]
+[], ["a"] => [["a",1]]
+["a"], ["b"] => [["a",-1],["b",1]]
+["a"], ["a"] => [["a",0]]
+["a"], ["a","b"] => [["a",0],["b",1]]
+["a"], ["b","a"] => [["b",1],["a",0]]
+["a","b"], ["a"] => [["a",0],["b",-1]]
+["b","a"], ["a"] => [["b",-1],["a",0]]
+["b","a"], ["a","c"] => [["b",-1],["a",0],["c",1]]
+["b","a"], ["a","c"] => [["b",-1],["a",0],["c",1]]
+["b","a"], ["a","b"] => [["a",0],["b",0]]
+["a","b","c"], ["a","d","c"] => [["a",0],["b",-1],["d",1],["c",0]]
+
 -- Running test case: Array.prototype.lastValue
 PASS: lastValue of a nonempty array should be the last value.
 PASS: lastValue of an empty array should be undefined.
index ca9c58f..01d8de1 100644 (file)
@@ -152,6 +152,35 @@ function test()
     });
 
     suite.addTestCase({
+        name: "Array.diffArrays",
+        test() {
+            function diff(initial, current) {
+                let actual = [];
+                Array.diffArrays(initial, current, (value, changed) => {
+                    actual.push([value, changed]);
+                });
+
+                InspectorTest.log(JSON.stringify(initial) + ", " + JSON.stringify(current) + " => " + JSON.stringify(actual));
+            }
+
+            diff(["a"], []);
+            diff([], ["a"]);
+            diff(["a"], ["b"]);
+            diff(["a"], ["a"]);
+            diff(["a"], ["a", "b"]);
+            diff(["a"], ["b", "a"]);
+            diff(["a", "b"], ["a"]);
+            diff(["b", "a"], ["a"]);
+            diff(["b", "a"], ["a", "c"]);
+            diff(["b", "a"], ["a", "c"]);
+            diff(["b", "a"], ["a", "b"]);
+            diff(["a", "b", "c"], ["a", "d", "c"]);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
         name: "Array.prototype.lastValue",
         test() {
             let object1 = {};
index ffddf7c..f954955 100644 (file)
@@ -1,3 +1,91 @@
+2019-01-28  Nikita Vasilyev  <nvasilyev@apple.com>
+
+        Web Inspector: Add Changes panel to Elements tab
+        https://bugs.webkit.org/show_bug.cgi?id=193803
+
+        Reviewed by Devin Rousso.
+
+        Introduce the new experimental Changes Panel. It shows a list of CSS changes
+        made via Web Inspector, so the changes could be copied to the source files.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Base/Setting.js:
+        * UserInterface/Base/Utilities.js:
+        (Array.diffArrays): Added.
+
+        * UserInterface/Controllers/CSSManager.js:
+        (WI.CSSManager):
+        (WI.CSSManager.prototype.get modifiedCSSRules):
+        (WI.CSSManager.prototype.addModifiedCSSRule):
+        (WI.CSSManager.prototype.removeModifiedCSSRule):
+        (WI.CSSManager.prototype._mainResourceDidChange):
+
+        * UserInterface/Main.html:
+        * UserInterface/Models/CSSProperty.js:
+        (WI.CSSProperty):
+        (WI.CSSProperty.prototype.remove):
+        (WI.CSSProperty.prototype.replaceWithText):
+        (WI.CSSProperty.prototype.commentOut):
+        (WI.CSSProperty.prototype.set text):
+        (WI.CSSProperty.prototype.get modified):
+        (WI.CSSProperty.prototype.set name):
+        (WI.CSSProperty.prototype.set rawValue):
+        (WI.CSSProperty.prototype.get initialState):
+        (WI.CSSProperty.prototype._updateOwnerStyleText):
+        (WI.CSSProperty.prototype._markModified):
+        Mark CSSProperty modified *before* making any changes to copy its initial state.
+
+        * UserInterface/Models/CSSRule.js:
+        (WI.CSSRule):
+        (WI.CSSRule.prototype.get id):
+        (WI.CSSRule.prototype.get initialState):
+        (WI.CSSRule.prototype.get stringId):
+        (WI.CSSRule.prototype.markModified):
+
+        * UserInterface/Models/CSSStyleDeclaration.js:
+        (WI.CSSStyleDeclaration):
+        (WI.CSSStyleDeclaration.prototype.get initialState):
+        (WI.CSSStyleDeclaration.prototype.get enabledProperties):
+        (WI.CSSStyleDeclaration.prototype.get properties):
+        (WI.CSSStyleDeclaration.prototype.set properties):
+        (WI.CSSStyleDeclaration.prototype.propertyForName):
+        (WI.CSSStyleDeclaration.prototype.newBlankProperty):
+        (WI.CSSStyleDeclaration.prototype.markModified):
+
+        * UserInterface/Views/ChangesDetailsSidebarPanel.css: Added.
+        (.sidebar > .panel.changes-panel):
+        (.sidebar > .panel.changes-panel:not(.empty)):
+        (.sidebar > .panel.changes-panel.empty):
+        (.changes-panel ins):
+        (.changes-panel del):
+        (.changes-panel del.css-property::before):
+        (.changes-panel ins.css-property::before):
+        (@media (prefers-color-scheme: dark)):
+
+        * UserInterface/Views/ChangesDetailsSidebarPanel.js: Added.
+        (WI.ChangesDetailsSidebarPanel):
+        (WI.ChangesDetailsSidebarPanel.prototype.inspect):
+        (WI.ChangesDetailsSidebarPanel.prototype.supportsDOMNode):
+        (WI.ChangesDetailsSidebarPanel.prototype.shown):
+        (WI.ChangesDetailsSidebarPanel.prototype.detached):
+        (WI.ChangesDetailsSidebarPanel.prototype.layout):
+        (WI.ChangesDetailsSidebarPanel.prototype._mainResourceDidChange):
+
+        * UserInterface/Views/ElementsTabContentView.js:
+        (WI.ElementsTabContentView):
+
+        * UserInterface/Views/SettingsTabContentView.js:
+        (WI.SettingsTabContentView.prototype._createExperimentalSettingsView):
+
+        * UserInterface/Views/SpreadsheetCSSStyleDeclarationEditor.css:
+        (.spreadsheet-style-declaration-editor .property):
+        (.spreadsheet-style-declaration-editor .property.modified):
+        (.spreadsheet-style-declaration-editor .property.modified:not(.selected)):
+        (@media (prefers-color-scheme: dark)):
+
+        * UserInterface/Views/SpreadsheetStyleProperty.js:
+        (WI.SpreadsheetStyleProperty.prototype.updateStatus):
+
 2019-01-26  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: handle CSS Color 4 color syntaxes
index bf92baf..ebe29d5 100644 (file)
@@ -169,6 +169,7 @@ localizedStrings["CSP Hash"] = "CSP Hash";
 localizedStrings["CSS"] = "CSS";
 localizedStrings["CSS Canvas"] = "CSS Canvas";
 localizedStrings["CSS canvas \u201C%s\u201D"] = "CSS canvas \u201C%s\u201D";
+localizedStrings["CSS hasn't been modified."] = "CSS hasn't been modified.";
 localizedStrings["Cached"] = "Cached";
 localizedStrings["Call Frames Truncated"] = "Call Frames Truncated";
 localizedStrings["Call Stack"] = "Call Stack";
@@ -187,6 +188,7 @@ localizedStrings["Capturing"] = "Capturing";
 localizedStrings["Catch Variables"] = "Catch Variables";
 localizedStrings["Categories"] = "Categories";
 localizedStrings["Certificate"] = "Certificate";
+localizedStrings["Changes"] = "Changes";
 localizedStrings["Character Data"] = "Character Data";
 localizedStrings["Charge \u201C%s\u201D to Callers"] = "Charge \u201C%s\u201D to Callers";
 localizedStrings["Checked"] = "Checked";
@@ -366,6 +368,7 @@ localizedStrings["Element overlaps other compositing element"] = "Element overla
 localizedStrings["Elements"] = "Elements";
 localizedStrings["Enable Breakpoint"] = "Enable Breakpoint";
 localizedStrings["Enable Breakpoints"] = "Enable Breakpoints";
+localizedStrings["Enable Changes Panel"] = "Enable Changes Panel";
 localizedStrings["Enable Computed Style Cascades"] = "Enable Computed Style Cascades";
 localizedStrings["Enable Event Listener"] = "Enable Event Listener";
 localizedStrings["Enable Layers Tab"] = "Enable Layers Tab";
index 0a8415a..1173fc8 100644 (file)
@@ -153,6 +153,7 @@ WI.settings = {
 
     // Experimental
     experimentalEnableComputedStyleCascades: new WI.Setting("experimental-enable-computed-style-cascades", false),
+    experimentalEnableChangesPanel: new WI.Setting("experimental-enable-changes-panel", false),
     experimentalEnableLayersTab: new WI.Setting("experimental-enable-layers-tab", false),
     experimentalEnableNewTabBar: new WI.Setting("experimental-enable-new-tab-bar", false),
     experimentalEnableAuditTab: new WI.Setting("experimental-enable-audit-tab", false),
index 9bf0894..c1dded5 100644 (file)
@@ -489,6 +489,67 @@ Object.defineProperty(Array, "shallowEqual",
     }
 });
 
+Object.defineProperty(Array, "diffArrays",
+{
+    value(initialArray, currentArray, onEach)
+    {
+        let initialSet = new Set(initialArray);
+        let currentSet = new Set(currentArray);
+        let indexInitial = 0;
+        let indexCurrent = 0;
+        let deltaInitial = 0;
+        let deltaCurrent = 0;
+
+        let i = 0;
+        while (true) {
+            if (indexInitial >= initialArray.length || indexCurrent >= currentArray.length)
+                break;
+
+            let initial = initialArray[indexInitial];
+            let current = currentArray[indexCurrent];
+
+            if (initial === current)
+                onEach(current, 0);
+            else if (currentSet.has(initial)) {
+                if (initialSet.has(current)) {
+                    // Moved.
+                    onEach(current, 0);
+                } else {
+                    // Added.
+                    onEach(current, 1);
+                    --i;
+                    ++deltaCurrent;
+                }
+            } else {
+                // Removed.
+                onEach(initial, -1);
+                if (!initialSet.has(current)) {
+                    // Added.
+                    onEach(current, 1);
+                } else {
+                    --i;
+                    ++deltaInitial;
+                }
+            }
+
+            ++i;
+            indexInitial = i + deltaInitial;
+            indexCurrent = i + deltaCurrent;
+        }
+
+        for (let i = indexInitial; i < initialArray.length; ++i) {
+            // Removed.
+            onEach(initialArray[i], -1);
+        }
+
+        for (let i = indexCurrent; i < currentArray.length; ++i) {
+            // Added.
+            onEach(currentArray[i], 1);
+        }
+
+    }
+});
+
 Object.defineProperty(Array.prototype, "lastValue",
 {
     get()
index 743220c..c00cda2 100644 (file)
@@ -45,6 +45,7 @@ WI.CSSManager = class CSSManager extends WI.Object
         this._styleSheetIdentifierMap = new Map;
         this._styleSheetFrameURLMap = new Map;
         this._nodeStylesMap = {};
+        this._modifiedCSSRules = new Map;
         this._defaultAppearance = null;
         this._forcedAppearance = null;
 
@@ -348,6 +349,21 @@ WI.CSSManager = class CSSManager extends WI.Object
         this.dispatchEventToListeners(WI.CSSManager.Event.DefaultAppearanceDidChange, {appearance});
     }
 
+    get modifiedCSSRules()
+    {
+        return Array.from(this._modifiedCSSRules.values());
+    }
+
+    addModifiedCSSRule(cssRule)
+    {
+        this._modifiedCSSRules.set(cssRule.stringId, cssRule);
+    }
+
+    removeModifiedCSSRule(cssRule)
+    {
+        this._modifiedCSSRules.delete(cssRule.stringId);
+    }
+
     // Protected
 
     mediaQueryResultChanged()
@@ -445,6 +461,8 @@ WI.CSSManager = class CSSManager extends WI.Object
         this._fetchedInitialStyleSheets = InspectorBackend.domains.CSS.hasEvent("styleSheetAdded");
         this._styleSheetIdentifierMap.clear();
         this._styleSheetFrameURLMap.clear();
+        this._modifiedCSSRules.clear();
+
         this._nodeStylesMap = {};
     }
 
index 33c5cbe..c97d00d 100644 (file)
@@ -54,6 +54,7 @@
     <link rel="stylesheet" href="Views/CanvasOverviewContentView.css">
     <link rel="stylesheet" href="Views/CanvasSidebarPanel.css">
     <link rel="stylesheet" href="Views/CanvasTabContentView.css">
+    <link rel="stylesheet" href="Views/ChangesDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/ChartDetailsSectionRow.css">
     <link rel="stylesheet" href="Views/CheckboxNavigationItem.css">
     <link rel="stylesheet" href="Views/CircleChart.css">
     <script src="Views/CanvasOverviewContentView.js"></script>
     <script src="Views/CanvasSidebarPanel.js"></script>
     <script src="Views/CanvasTreeElement.js"></script>
+    <script src="Views/ChangesDetailsSidebarPanel.js"></script>
     <script src="Views/ChartDetailsSectionRow.js"></script>
     <script src="Views/CheckboxNavigationItem.js"></script>
     <script src="Views/CircleChart.js"></script>
index 172a134..1be38e5 100644 (file)
@@ -31,6 +31,7 @@ WI.CSSProperty = class CSSProperty extends WI.Object
 
         this._ownerStyle = null;
         this._index = index;
+        this._initialState = null;
 
         this.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, true);
     }
@@ -125,6 +126,8 @@ WI.CSSProperty = class CSSProperty extends WI.Object
 
     remove()
     {
+        this._markModified();
+
         // Setting name or value to an empty string removes the entire CSSProperty.
         this._name = "";
         const forceRemove = true;
@@ -133,6 +136,8 @@ WI.CSSProperty = class CSSProperty extends WI.Object
 
     replaceWithText(text)
     {
+        this._markModified();
+
         this._updateOwnerStyleText(this._text, text, true);
     }
 
@@ -142,6 +147,7 @@ WI.CSSProperty = class CSSProperty extends WI.Object
         if (this._enabled === !disabled)
             return;
 
+        this._markModified();
         this._enabled = !disabled;
 
         if (disabled)
@@ -160,6 +166,7 @@ WI.CSSProperty = class CSSProperty extends WI.Object
         if (this._text === newText)
             return;
 
+        this._markModified();
         this._updateOwnerStyleText(this._text, newText);
         this._text = newText;
     }
@@ -172,6 +179,11 @@ WI.CSSProperty = class CSSProperty extends WI.Object
         return `${this._name}: ${this._rawValue};`;
     }
 
+    get modified()
+    {
+        return !!this._initialState;
+    }
+
     get name()
     {
         return this._name;
@@ -182,6 +194,7 @@ WI.CSSProperty = class CSSProperty extends WI.Object
         if (name === this._name)
             return;
 
+        this._markModified();
         this._name = name;
         this._updateStyleText();
     }
@@ -215,6 +228,8 @@ WI.CSSProperty = class CSSProperty extends WI.Object
         if (value === this._rawValue)
             return;
 
+        this._markModified();
+
         this._rawValue = value;
         this._value = undefined;
         this._updateStyleText();
@@ -272,6 +287,11 @@ WI.CSSProperty = class CSSProperty extends WI.Object
     get variable() { return this._variable; }
     get styleSheetTextRange() { return this._styleSheetTextRange; }
 
+    get initialState()
+    {
+        return this._initialState;
+    }
+
     get editable()
     {
         return !!(this._styleSheetTextRange && this._ownerStyle && this._ownerStyle.styleSheetTextRange);
@@ -349,6 +369,8 @@ WI.CSSProperty = class CSSProperty extends WI.Object
 
     _updateOwnerStyleText(oldText, newText, forceRemove = false)
     {
+        console.assert(this.modified, "CSSProperty was modified without saving initial state.");
+
         if (oldText === newText) {
             if (forceRemove) {
                 const lineDelta = 0;
@@ -403,6 +425,30 @@ WI.CSSProperty = class CSSProperty extends WI.Object
             break;
         }
     }
+
+    _markModified()
+    {
+        if (this.modified)
+            return;
+
+        this._initialState = new WI.CSSProperty(
+            this._index,
+            this._text,
+            this._name,
+            this._rawValue,
+            this._priority,
+            this._enabled,
+            this._overridden,
+            this._implicit,
+            this._anonymous,
+            this._valid,
+            this._styleSheetTextRange);
+
+        if (this._ownerStyle) {
+            this._ownerStyle.markModified();
+            this._initialState.ownerStyle = this._ownerStyle.initialState;
+        }
+    }
 };
 
 WI.CSSProperty.Event = {
index fc5dd58..375845b 100644 (file)
@@ -35,15 +35,20 @@ WI.CSSRule = class CSSRule extends WI.Object
         this._ownerStyleSheet = ownerStyleSheet || null;
         this._id = id || null;
         this._type = type || null;
+        this._initialState = null;
 
         this.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList, true);
     }
 
     // Public
 
-    get id()
+    get id() { return this._id; }
+    get initialState() { return this._initialState; }
+
+    get stringId()
     {
-        return this._id;
+        if (this._id)
+            return this._id.styleSheetId + "/" + this._id.ordinal;
     }
 
     get ownerStyleSheet()
@@ -147,6 +152,27 @@ WI.CSSRule = class CSSRule extends WI.Object
         return Object.shallowEqual(this._id, rule.id);
     }
 
+    markModified()
+    {
+        if (this._initialState)
+            return;
+
+        let initialStyle = this._style.initialState || this._style;
+        this._initialState = new WI.CSSRule(
+            this._nodeStyles,
+            this._ownerStyleSheet,
+            this._id,
+            this._type,
+            this._sourceCodeLocation,
+            this._selectorText,
+            this._selectors,
+            this._matchedSelectorIndices,
+            initialStyle,
+            this._mediaList);
+
+        WI.cssManager.addModifiedCSSRule(this);
+    }
+
     // Protected
 
     get nodeStyles()
index 99780b9..56c1067 100644 (file)
@@ -40,12 +40,13 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
         this._node = node || null;
         this._inherited = inherited || false;
 
+        this._initialState = null;
         this._locked = false;
         this._pendingProperties = [];
         this._propertyNameMap = {};
 
         this._properties = [];
-        this._enabledProperties = [];
+        this._enabledProperties = null;
         this._visibleProperties = null;
 
         this.update(text, properties, styleSheetTextRange, {dontFireEvents: true});
@@ -53,6 +54,8 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
 
     // Public
 
+    get initialState() { return this._initialState; }
+
     get id()
     {
         return this._id;
@@ -116,11 +119,11 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
 
         this._text = text;
         this._properties = properties;
-        this._enabledProperties = properties.filter((property) => property.enabled);
 
         this._styleSheetTextRange = styleSheetTextRange;
         this._propertyNameMap = {};
 
+        this._enabledProperties = null;
         this._visibleProperties = null;
 
         let editable = this.editable;
@@ -141,7 +144,7 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
         }
 
         for (let oldProperty of oldProperties) {
-            if (this._enabledProperties.includes(oldProperty))
+            if (this.enabledProperties.includes(oldProperty))
                 continue;
 
             // Clear the index, since it is no longer valid.
@@ -205,10 +208,26 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
 
     get enabledProperties()
     {
+        if (!this._enabledProperties)
+            this._enabledProperties = this._properties.filter((property) => property.enabled);
+
         return this._enabledProperties;
     }
 
-    get properties() { return this._properties; }
+    get properties()
+    {
+        return this._properties;
+    }
+
+    set properties(properties)
+    {
+        if (properties === this._properties)
+            return;
+
+        this._properties = properties;
+        this._enabledProperties = null;
+        this._visibleProperties = null;
+    }
 
     get visibleProperties()
     {
@@ -268,7 +287,7 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
 
         var bestMatchProperty = null;
 
-        findMatch(this._enabledProperties);
+        findMatch(this.enabledProperties);
 
         if (bestMatchProperty)
             return bestMatchProperty;
@@ -296,6 +315,7 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
         let valid = false;
         let styleSheetTextRange = this._rangeAfterPropertyAtIndex(propertyIndex - 1);
 
+        this.markModified();
         let property = new WI.CSSProperty(propertyIndex, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
 
         this._properties.insertAtIndex(property, propertyIndex);
@@ -307,6 +327,29 @@ WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
         return property;
     }
 
+    markModified()
+    {
+        let properties = this._initialState ? this._initialState.properties : this._properties;
+
+        if (!this._initialState) {
+            this._initialState = new WI.CSSStyleDeclaration(
+                    this._nodeStyles,
+                    this._ownerStyleSheet,
+                    this._id,
+                    this._type,
+                    this._node,
+                    this._inherited,
+                    this._text,
+                    [], // Passing CSS properties here would change their ownerStyle.
+                    this._styleSheetTextRange);
+        }
+
+        this._initialState.properties = properties.map((property) => { return property.initialState || property });
+
+        if (this._ownerRule)
+            this._ownerRule.markModified();
+    }
+
     shiftPropertiesAfter(cssProperty, lineDelta, columnDelta, propertyWasRemoved)
     {
         // cssProperty.index could be set to NaN by WI.CSSStyleDeclaration.prototype.update.
diff --git a/Source/WebInspectorUI/UserInterface/Views/ChangesDetailsSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/ChangesDetailsSidebarPanel.css
new file mode 100644 (file)
index 0000000..8dabd97
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+.sidebar > .panel.changes-panel {
+    padding: 8px 10px;
+    white-space: pre-wrap;
+    overflow-y: auto;
+}
+
+.sidebar > .panel.changes-panel:not(.empty) {
+    font: 11px Menlo, monospace;
+    -webkit-user-select: text;
+}
+
+.sidebar > .panel.changes-panel.empty {
+    text-align: center;
+}
+
+.changes-panel ins {
+    color: hsl(90, 61%, 25%);
+    background-color: hsl(70, 65%, 85%);
+    text-decoration: none;
+}
+
+.changes-panel del {
+    color: hsl(0, 100%, 35%);
+    background-color: hsl(5, 78%, 91%);
+    text-decoration: none;
+}
+
+.changes-panel del.css-property::before {
+    content: "- ";
+    position: absolute;
+    pointer-events: none;
+}
+
+.changes-panel ins.css-property::before {
+    content: "+ ";
+    position: absolute;
+    pointer-events: none;
+}
+
+@media (prefers-color-scheme: dark) {
+    .changes-panel ins {
+        color: hsl(70, 64%, 70%);
+        background-color: hsl(89, 40%, 19%);
+    }
+
+    .changes-panel del {
+        color: hsl(0, 84%, 75%);
+        background-color: hsl(5, 40%, 25%);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/ChangesDetailsSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/ChangesDetailsSidebarPanel.js
new file mode 100644 (file)
index 0000000..3f8ba41
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+WI.ChangesDetailsSidebarPanel = class ChangesDetailsSidebarPanel extends WI.DetailsSidebarPanel
+{
+    constructor()
+    {
+        super("changes-details", WI.UIString("Changes"));
+
+        this.element.classList.add("changes-panel");
+    }
+
+    // Public
+
+    inspect(objects)
+    {
+        return true;
+    }
+
+    supportsDOMNode(nodeToInspect)
+    {
+        // Display Changes panel regardless of the selected DOM node.
+        return true;
+    }
+
+    shown()
+    {
+        // `shown` may get called before initialLayout when Elements tab is opened.
+        // When Changes panel is selected, `shown` is called and this time it's after initialLayout.
+        if (this.didInitialLayout) {
+            this.needsLayout();
+            WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
+        }
+
+        super.shown();
+    }
+
+    detached()
+    {
+        super.detached();
+
+        WI.Frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
+    }
+
+    // Protected
+
+    layout()
+    {
+        super.layout();
+
+        this.element.removeChildren();
+
+        let cssRules = WI.cssManager.modifiedCSSRules;
+
+        this.element.classList.toggle("empty", !cssRules.length);
+        if (!cssRules.length) {
+            this.element.textContent = WI.UIString("CSS hasn't been modified.");
+            return;
+        }
+
+        let indent = WI.indentString();
+
+        let appendPropertyElement = (tagName, text) => {
+            let propertyElement = document.createElement(tagName);
+            propertyElement.className = "css-property";
+            propertyElement.append(indent, text);
+            this.element.append(propertyElement, "\n");
+        };
+
+        for (let cssRule of cssRules) {
+            let selectorElement = document.createElement("span");
+            selectorElement.append(cssRule.selectorText, " {\n");
+            this.element.append(selectorElement);
+
+            let initialCSSProperties = cssRule.initialState.style.visibleProperties;
+            let cssProperties = cssRule.style.visibleProperties;
+
+            Array.diffArrays(initialCSSProperties, cssProperties, (cssProperty, action) => {
+                if (action === 0) {
+                    if (cssProperty.modified) {
+                        appendPropertyElement("del", cssProperty.initialState.formattedText);
+                        appendPropertyElement("ins", cssProperty.formattedText);
+                    } else
+                        appendPropertyElement("span", cssProperty.formattedText);
+                } else if (action === 1)
+                    appendPropertyElement("ins", cssProperty.formattedText);
+                else if (action === -1)
+                    appendPropertyElement("del", cssProperty.formattedText);
+            });
+
+            this.element.append("}\n\n");
+        }
+    }
+
+    // Private
+
+    _mainResourceDidChange(event)
+    {
+        if (!event.target.isMainFrame())
+            return;
+
+        this.needsLayout();
+    }
+};
index 8ce29ee..2eb912a 100644 (file)
@@ -28,8 +28,11 @@ WI.ElementsTabContentView = class ElementsTabContentView extends WI.ContentBrows
     constructor(identifier)
     {
         let tabBarItem = WI.GeneralTabBarItem.fromTabInfo(WI.ElementsTabContentView.tabInfo());
-        let detailsSidebarPanelConstructors = [WI.RulesStyleDetailsSidebarPanel, WI.ComputedStyleDetailsSidebarPanel, WI.DOMNodeDetailsSidebarPanel];
 
+        let detailsSidebarPanelConstructors = [WI.RulesStyleDetailsSidebarPanel, WI.ComputedStyleDetailsSidebarPanel];
+        if (WI.settings.experimentalEnableChangesPanel.value)
+            detailsSidebarPanelConstructors.push(WI.ChangesDetailsSidebarPanel);
+        detailsSidebarPanelConstructors.push(WI.DOMNodeDetailsSidebarPanel);
         if (window.LayerTreeAgent)
             detailsSidebarPanelConstructors.push(WI.LayerTreeDetailsSidebarPanel);
 
index f5f815b..cd23b4e 100644 (file)
@@ -256,6 +256,7 @@ WI.SettingsTabContentView = class SettingsTabContentView extends WI.TabContentVi
         if (window.CSSAgent) {
             let group = experimentalSettingsView.addGroup(WI.UIString("Styles Sidebar:"));
             group.addSetting(WI.settings.experimentalEnableComputedStyleCascades, WI.UIString("Enable Computed Style Cascades"));
+            group.addSetting(WI.settings.experimentalEnableChangesPanel, WI.UIString("Enable Changes Panel"));
             experimentalSettingsView.addSeparator();
         }
 
@@ -290,6 +291,7 @@ WI.SettingsTabContentView = class SettingsTabContentView extends WI.TabContentVi
         }
 
         listenForChange(WI.settings.experimentalEnableComputedStyleCascades);
+        listenForChange(WI.settings.experimentalEnableChangesPanel);
         listenForChange(WI.settings.experimentalEnableLayersTab);
         listenForChange(WI.settings.experimentalEnableNewTabBar);
 
index 17f508a..98b7498 100644 (file)
@@ -38,6 +38,7 @@
 .spreadsheet-style-declaration-editor .property {
     padding-right: var(--css-declaration-horizontal-padding);
     padding-left: calc(var(--css-declaration-horizontal-padding) + 17px);
+    border-right: 2px solid transparent;
     border-left: 1px solid transparent;
     outline: none;
 }
     -webkit-clip-path: polygon(0% 50%, 6px 0%, 100% 0%, 100% 100%, 6px 100%);
 }
 
+.spreadsheet-style-declaration-editor .property.modified {
+    border-right-color: hsl(120, 100%, 40%);
+}
+
+.spreadsheet-style-declaration-editor .property.modified:not(.selected) {
+    background-color: hsl(90, 100%, 93%);
+}
+
 .spreadsheet-style-declaration-editor .property.selected {
     background-color: var(--background-color-selected);
 }
@@ -188,4 +197,8 @@ body:matches(.window-docked-inactive, .window-inactive) .spreadsheet-style-decla
     .spreadsheet-style-declaration-editor :matches(.name, .value).editing {
         outline-color: var(--background-color-secondary) !important;
     }
+
+    .spreadsheet-style-declaration-editor .property.modified:not(.selected) {
+        background-color: hsl(106, 13%, 25%);
+    }
 }
index 611d816..4411e99 100644 (file)
@@ -295,6 +295,9 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         if (!this._property.enabled)
             classNames.push("disabled");
 
+        if (this._property.modified)
+            classNames.push("modified");
+
         if (this._selected)
             classNames.push("selected");