Web Inspector: Local Resource Overrides: UI for overriding image and font resource content
https://bugs.webkit.org/show_bug.cgi?id=202016
<rdar://problem/55541475>

Reviewed by Devin Rousso.

Source/WebInspectorUI:

Extend SourceCodeRevision to be a (content, base64Encoded, mimeType) tuple and
make clients update the revision content more explicitly (`updateRevisionContent`).
This also includes `blobContent` as a more explicit way to get the content as
a Blob, which may not always be desired.

Switch LocalResource use the originalRevision / currentRevision instead of
keeping its own localContent / localContentIsBase64Encoded properties.

Introduce a `DropZoneView` to simplify handling of presenting a drop zone
over a specific element. And use it for the ImageResourceContentView for local
resource overrides to accept new content.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Main.html:
New strings and resources.

* .eslintrc:
* UserInterface/Base/BlobUtilities.js: Added.
(WI.BlobUtilities.blobForContent):
(WI.BlobUtilities.decodeBase64ToBlob):
(WI.BlobUtilities.textToBlob):
(WI.BlobUtilities.blobAsText):
(WI.BlobUtilities):
* UserInterface/Base/FileUtilities.js:
(WI.FileUtilities.async.readDataURL):
(WI.FileUtilities):
* UserInterface/Base/MIMETypeUtilities.js:
(WI.fileExtensionForFilename):
(WI.fileExtensionForURL):
* UserInterface/Base/Utilities.js:
Move around or introduce some minor utilities.

* UserInterface/Models/SourceCodeRevision.js:
(WI.SourceCodeRevision):
(WI.SourceCodeRevision.prototype.get sourceCode):
(WI.SourceCodeRevision.prototype.get content):
(WI.SourceCodeRevision.prototype.get base64Encoded):
(WI.SourceCodeRevision.prototype.get mimeType):
(WI.SourceCodeRevision.prototype.get blobContent):
(WI.SourceCodeRevision.prototype.updateRevisionContent):
(WI.SourceCodeRevision.prototype.copy):
(WI.SourceCodeRevision.prototype.set content): Deleted.
Data is now a (content, base64Encoded, mimeType) tuple.

* UserInterface/Controllers/NetworkManager.js:
(WI.NetworkManager.prototype.responseIntercepted):
(WI.NetworkManager.prototype._handleResourceContentDidChange):
(WI.NetworkManager.prototype._persistLocalResourceOverrideSoonAfterContentChange): Deleted.
This is now a unified resource content change path without anything special for
local resource overrides.

* UserInterface/Models/LocalResource.js:
(WI.LocalResource.prototype.toJSON):
(WI.LocalResource.prototype.requestContentFromBackend):
(WI.LocalResource.prototype.handleCurrentRevisionContentChange):
(WI.LocalResource):
(WI.LocalResource.prototype.get localContent): Deleted.
(WI.LocalResource.prototype.get localContentIsBase64Encoded): Deleted.
(WI.LocalResource.prototype.hasContent): Deleted.
(WI.LocalResource.prototype.setContent): Deleted.
(WI.LocalResource.prototype.updateOverrideContent): Deleted.
Use originalRevision / currentRevision as appropriate.

* UserInterface/Views/DropZoneView.css: Added.
(.drop-zone):
(.drop-zone.visible):
(@media (prefers-color-scheme: dark)):
* UserInterface/Views/DropZoneView.js: Added.
(WI.DropZoneView):
(WI.DropZoneView.prototype.get delegate):
(WI.DropZoneView.prototype.get targetElement):
(WI.DropZoneView.prototype.set targetElement):
(WI.DropZoneView.prototype.initialLayout):
(WI.DropZoneView.prototype._startActiveDrag):
(WI.DropZoneView.prototype._stopActiveDrag):
(WI.DropZoneView.prototype._handleDragEnter):
(WI.DropZoneView.prototype._handleDragLeave):
(WI.DropZoneView.prototype._handleDragOver):
(WI.DropZoneView.prototype._handleDrop):
Simplified handling of a drop zone.

* UserInterface/Views/ResourceContentView.js:
(WI.ResourceContentView.prototype.removeLoadingIndicator):
More safely remove children and subviews.

(WI.ResourceContentView):
(WI.ResourceContentView.prototype.get resource):
(WI.ResourceContentView.prototype.get navigationItems):
(WI.ResourceContentView.prototype.localResourceOverrideInitialContent):
(WI.ResourceContentView.prototype.closed):
(WI.ResourceContentView.prototype.removeLoadingIndicator):
(WI.ResourceContentView.prototype._contentAvailable):
(WI.ResourceContentView.prototype._issueWasAdded):
(WI.ResourceContentView.prototype.async._handleCreateLocalResourceOverride):
(WI.ResourceContentView.prototype._handleRemoveLocalResourceOverride):
(WI.ResourceContentView.prototype._handleLocalResourceOverrideChanged):
(WI.ResourceContentView.prototype._mouseWasClicked):
* UserInterface/Views/TextResourceContentView.js:
(WI.TextResourceContentView):
(WI.TextResourceContentView.prototype.get navigationItems):
(WI.TextResourceContentView.prototype.localResourceOverrideInitialContent):
(WI.TextResourceContentView.prototype._contentWillPopulate):
(WI.TextResourceContentView.prototype._contentDidPopulate):
(WI.TextResourceContentView.prototype._textEditorContentDidChange):
(WI.TextResourceContentView.prototype._shouldBeEditable):
(WI.TextResourceContentView.prototype.async._handleCreateLocalResourceOverride): Deleted.
(WI.TextResourceContentView.prototype._handleRemoveLocalResourceOverride): Deleted.
(WI.TextResourceContentView.prototype._handleLocalResourceOverrideChanged): Deleted.
Extract generalized local resource override properties into the ResourceContentView base class.

* UserInterface/Views/FontResourceContentView.css:
(.content-view.resource.font):
(.content-view.resource.font > .drop-zone):
(.content-view.resource.font > .preview-container):
(.content-view.resource.font .preview):
* UserInterface/Views/FontResourceContentView.js:
(WI.FontResourceContentView):
(WI.FontResourceContentView.prototype.contentAvailable):
(WI.FontResourceContentView.prototype.shown):
(WI.FontResourceContentView.prototype.hidden):
(WI.FontResourceContentView.prototype.closed):
(WI.FontResourceContentView.prototype.layout):
(WI.FontResourceContentView.prototype._updatePreviewElement.createMetricElement):
(WI.FontResourceContentView.prototype._updatePreviewElement):
(WI.FontResourceContentView.prototype.dropZoneShouldAppearForDragEvent):
(WI.FontResourceContentView.prototype.dropZoneHandleDrop):
Create a drop zone that will update the font local resource override content.

* UserInterface/Views/ImageResourceContentView.css:
(.content-view.resource.image):
(.content-view.resource.image > .drop-zone):
(.content-view.resource.image > .img-container):
(.content-view.resource.image img):
* UserInterface/Views/ImageResourceContentView.js:
(WI.ImageResourceContentView):
(WI.ImageResourceContentView.prototype.get navigationItems):
(WI.ImageResourceContentView.prototype.contentAvailable):
(WI.ImageResourceContentView.prototype.closed):
(WI.ImageResourceContentView.prototype.dropZoneShouldAppearForDragEvent):
(WI.ImageResourceContentView.prototype.dropZoneHandleDrop):
Create a drop zone that will update the image local resource override content.

* UserInterface/Models/Script.js:
(WI.Script.prototype.get mimeType):
Seems like this should have a default value given there may not be a resource.

* UserInterface/Views/LocalResourceOverridePopover.js:
(WI.LocalResourceOverridePopover.prototype.show):
Better handling here, since the utilities expects a number not a string.

* UserInterface/Models/Resource.js:
(WI.Resource.prototype.createObjectURL):
* UserInterface/Views/LocalResourceOverrideTreeElement.js:
(WI.LocalResourceOverrideTreeElement.prototype.willDismissPopover):
Use currentRevision more appropriately.

* UserInterface/Models/SourceCode.js:
(WI.SourceCode.prototype._processContent):
* UserInterface/Views/TextResourceContentView.js:
(WI.TextResourceContentView.prototype._textEditorContentDidChange):
* UserInterface/Controllers/CSSManager.js:
(WI.CSSManager.prototype._resourceContentDidChange.applyStyleSheetChanges.styleSheetFound):
(WI.CSSManager.prototype._resourceContentDidChange.applyStyleSheetChanges):
(WI.CSSManager.prototype._resourceContentDidChange):
(WI.CSSManager.prototype._updateResourceContent.fetchedStyleSheetContent):
Update revision content more explicitly.

LayoutTests:

* inspector/unit-tests/mimetype-utilities-expected.txt:
* inspector/unit-tests/mimetype-utilities.html:
Test new utilities.

* http/tests/inspector/network/fetch-response-body.html:
* http/tests/inspector/network/xhr-response-body.html:
Renamed utilities.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251024 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index b0f0594..5c15fc1 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,19 @@
+2019-10-10  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Local Resource Overrides: UI for overriding image and font resource content
+        https://bugs.webkit.org/show_bug.cgi?id=202016
+        <rdar://problem/55541475>
+
+        Reviewed by Devin Rousso.
+
+        * inspector/unit-tests/mimetype-utilities-expected.txt:
+        * inspector/unit-tests/mimetype-utilities.html:
+        Test new utilities.
+
+        * http/tests/inspector/network/fetch-response-body.html:
+        * http/tests/inspector/network/xhr-response-body.html:
+        Renamed utilities.
+
 2019-10-11  Dean Jackson  <dino@apple.com>
 
         Layout test fast/events/touch/ios/tap-with-active-listener-inside-window-with-passive-listener.html is a flaky failure
diff --git a/LayoutTests/http/tests/inspector/network/fetch-response-body.html b/LayoutTests/http/tests/inspector/network/fetch-response-body.html
index c59c6a3..fdfc593 100644
--- a/LayoutTests/http/tests/inspector/network/fetch-response-body.html
+++ b/LayoutTests/http/tests/inspector/network/fetch-response-body.html
@@ -42,7 +42,7 @@
                 resolve(content)
                 return;
             }
-            blobAsText(content, (text) => {
+            WI.BlobUtilities.blobAsText(content, (text) => {
                 resolve(text);
             });
         });
diff --git a/LayoutTests/http/tests/inspector/network/xhr-response-body.html b/LayoutTests/http/tests/inspector/network/xhr-response-body.html
index dd49e832..99dbd0e 100644
--- a/LayoutTests/http/tests/inspector/network/xhr-response-body.html
+++ b/LayoutTests/http/tests/inspector/network/xhr-response-body.html
@@ -56,7 +56,7 @@
                 resolve(content)
                 return;
             }
-            blobAsText(content, (text) => {
+            WI.BlobUtilities.blobAsText(content, (text) => {
                 resolve(text);
             });
         });
diff --git a/LayoutTests/inspector/unit-tests/mimetype-utilities-expected.txt b/LayoutTests/inspector/unit-tests/mimetype-utilities-expected.txt
index 4fcd142..905cfc3 100644
--- a/LayoutTests/inspector/unit-tests/mimetype-utilities-expected.txt
+++ b/LayoutTests/inspector/unit-tests/mimetype-utilities-expected.txt
@@ -1,5 +1,15 @@
 
 == Running test suite: MIMETypeUtilities
+-- Running test case: fileExtensionForFilename
+PASS: File extension for null filename should be null.
+PASS: File extension for filename without a period should be null.
+PASS: File extension for filename ending in a period should be null.
+PASS: File extension for "foo.xyz" should be "xyz".
+PASS: File extension for "image.png" should be "png".
+PASS: File extension for "image.png" should be "gif".
+PASS: File extension for "script.js" should be "js".
+PASS: File extension for "script.min.js" should be "js".
+
 -- Running test case: fileExtensionForURL
 PASS: File extension for null URL should be null.
 PASS: File extension for invalid URL should be null.
diff --git a/LayoutTests/inspector/unit-tests/mimetype-utilities.html b/LayoutTests/inspector/unit-tests/mimetype-utilities.html
index 78e43c1..c181029 100644
--- a/LayoutTests/inspector/unit-tests/mimetype-utilities.html
+++ b/LayoutTests/inspector/unit-tests/mimetype-utilities.html
@@ -8,9 +8,26 @@
     let suite = InspectorTest.createSyncSuite("MIMETypeUtilities");
 
     suite.addTestCase({
+        name: "fileExtensionForFilename",
+        test() {
+            InspectorTest.expectNull(WI.fileExtensionForFilename(null), `File extension for null filename should be null.`);
+            InspectorTest.expectEqual(WI.fileExtensionForURL("test"), null, `File extension for filename without a period should be null.`);
+            InspectorTest.expectEqual(WI.fileExtensionForURL("test."), null, `File extension for filename ending in a period should be null.`);
+
+            InspectorTest.expectEqual(WI.fileExtensionForFilename("foo.xyz"), "xyz", `File extension for "foo.xyz" should be "xyz".`);
+            InspectorTest.expectEqual(WI.fileExtensionForFilename("image.png"), "png", `File extension for "image.png" should be "png".`);
+            InspectorTest.expectEqual(WI.fileExtensionForFilename("image.gif"), "gif", `File extension for "image.png" should be "gif".`);
+            InspectorTest.expectEqual(WI.fileExtensionForFilename("script.js"), "js", `File extension for "script.js" should be "js".`);
+            InspectorTest.expectEqual(WI.fileExtensionForFilename("script.min.js"), "js", `File extension for "script.min.js" should be "js".`);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
         name: "fileExtensionForURL",
         test() {
-            InspectorTest.expectEqual(WI.fileExtensionForURL(null), null, `File extension for null URL should be null.`);
+            InspectorTest.expectNull(WI.fileExtensionForURL(null), `File extension for null URL should be null.`);
             InspectorTest.expectEqual(WI.fileExtensionForURL("invalid-url"), null, `File extension for invalid URL should be null.`);
             InspectorTest.expectEqual(WI.fileExtensionForURL("https://example.com"), null, `File extension for URL without last path component should be null.`);
             InspectorTest.expectEqual(WI.fileExtensionForURL("https://example.com/"), null, `File extension for URL without last path component should be null.`);
diff --git a/Source/WebInspectorUI/.eslintrc b/Source/WebInspectorUI/.eslintrc
index 50a2aab..d485ebf 100644
--- a/Source/WebInspectorUI/.eslintrc
+++ b/Source/WebInspectorUI/.eslintrc
@@ -111,7 +111,6 @@
         // Utilities
         "appendWebInspectorConsoleEvaluationSourceURL": true,
         "appendWebInspectorSourceURL": true,
-        "blobAsText": true,
         "clamp": true,
         "doubleQuotedString": true,
         "ellipsis": true,
@@ -132,7 +131,6 @@
         "parseMIMEType": true,
         "resolveDotsInPath": true,
         "simpleGlobStringToRegExp": true,
-        "textToBlob": true,
         "timestamp": true,
         "zeroWidthSpace": true,
 
@@ -141,7 +139,6 @@
 
         // URL Utilities
         "absoluteURL": true,
-        "decodeBase64ToBlob": true,
         "parseDataURL": true,
         "parseLocationQueryParameters": true,
         "parseQueryString": true,
diff --git a/Source/WebInspectorUI/ChangeLog b/Source/WebInspectorUI/ChangeLog
index d6a8c2d..441c23d 100644
--- a/Source/WebInspectorUI/ChangeLog
+++ b/Source/WebInspectorUI/ChangeLog
@@ -1,3 +1,178 @@
+2019-10-10  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Local Resource Overrides: UI for overriding image and font resource content
+        https://bugs.webkit.org/show_bug.cgi?id=202016
+        <rdar://problem/55541475>
+
+        Reviewed by Devin Rousso.
+
+        Extend SourceCodeRevision to be a (content, base64Encoded, mimeType) tuple and
+        make clients update the revision content more explicitly (`updateRevisionContent`).
+        This also includes `blobContent` as a more explicit way to get the content as
+        a Blob, which may not always be desired.
+
+        Switch LocalResource use the originalRevision / currentRevision instead of
+        keeping its own localContent / localContentIsBase64Encoded properties.
+
+        Introduce a `DropZoneView` to simplify handling of presenting a drop zone
+        over a specific element. And use it for the ImageResourceContentView for local
+        resource overrides to accept new content.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        New strings and resources.
+
+        * .eslintrc:
+        * UserInterface/Base/BlobUtilities.js: Added.
+        (WI.BlobUtilities.blobForContent):
+        (WI.BlobUtilities.decodeBase64ToBlob):
+        (WI.BlobUtilities.textToBlob):
+        (WI.BlobUtilities.blobAsText):
+        (WI.BlobUtilities):
+        * UserInterface/Base/FileUtilities.js:
+        (WI.FileUtilities.async.readDataURL):
+        (WI.FileUtilities):
+        * UserInterface/Base/MIMETypeUtilities.js:
+        (WI.fileExtensionForFilename):
+        (WI.fileExtensionForURL):
+        * UserInterface/Base/Utilities.js:
+        Move around or introduce some minor utilities.
+
+        * UserInterface/Models/SourceCodeRevision.js:
+        (WI.SourceCodeRevision):
+        (WI.SourceCodeRevision.prototype.get sourceCode):
+        (WI.SourceCodeRevision.prototype.get content):
+        (WI.SourceCodeRevision.prototype.get base64Encoded):
+        (WI.SourceCodeRevision.prototype.get mimeType):
+        (WI.SourceCodeRevision.prototype.get blobContent):
+        (WI.SourceCodeRevision.prototype.updateRevisionContent):
+        (WI.SourceCodeRevision.prototype.copy):
+        (WI.SourceCodeRevision.prototype.set content): Deleted.
+        Data is now a (content, base64Encoded, mimeType) tuple.
+
+        * UserInterface/Controllers/NetworkManager.js:
+        (WI.NetworkManager.prototype.responseIntercepted):
+        (WI.NetworkManager.prototype._handleResourceContentDidChange):
+        (WI.NetworkManager.prototype._persistLocalResourceOverrideSoonAfterContentChange): Deleted.
+        This is now a unified resource content change path without anything special for
+        local resource overrides.
+
+        * UserInterface/Models/LocalResource.js:
+        (WI.LocalResource.prototype.toJSON):
+        (WI.LocalResource.prototype.requestContentFromBackend):
+        (WI.LocalResource.prototype.handleCurrentRevisionContentChange):
+        (WI.LocalResource):
+        (WI.LocalResource.prototype.get localContent): Deleted.
+        (WI.LocalResource.prototype.get localContentIsBase64Encoded): Deleted.
+        (WI.LocalResource.prototype.hasContent): Deleted.
+        (WI.LocalResource.prototype.setContent): Deleted.
+        (WI.LocalResource.prototype.updateOverrideContent): Deleted.
+        Use originalRevision / currentRevision as appropriate.
+
+        * UserInterface/Views/DropZoneView.css: Added.
+        (.drop-zone):
+        (.drop-zone.visible):
+        (@media (prefers-color-scheme: dark)):
+        * UserInterface/Views/DropZoneView.js: Added.
+        (WI.DropZoneView):
+        (WI.DropZoneView.prototype.get delegate):
+        (WI.DropZoneView.prototype.get targetElement):
+        (WI.DropZoneView.prototype.set targetElement):
+        (WI.DropZoneView.prototype.initialLayout):
+        (WI.DropZoneView.prototype._startActiveDrag):
+        (WI.DropZoneView.prototype._stopActiveDrag):
+        (WI.DropZoneView.prototype._handleDragEnter):
+        (WI.DropZoneView.prototype._handleDragLeave):
+        (WI.DropZoneView.prototype._handleDragOver):
+        (WI.DropZoneView.prototype._handleDrop):
+        Simplified handling of a drop zone.
+
+        * UserInterface/Views/ResourceContentView.js:
+        (WI.ResourceContentView.prototype.removeLoadingIndicator):
+        More safely remove children and subviews.
+
+        (WI.ResourceContentView):
+        (WI.ResourceContentView.prototype.get resource):
+        (WI.ResourceContentView.prototype.get navigationItems):
+        (WI.ResourceContentView.prototype.localResourceOverrideInitialContent):
+        (WI.ResourceContentView.prototype.closed):
+        (WI.ResourceContentView.prototype.removeLoadingIndicator):
+        (WI.ResourceContentView.prototype._contentAvailable):
+        (WI.ResourceContentView.prototype._issueWasAdded):
+        (WI.ResourceContentView.prototype.async._handleCreateLocalResourceOverride):
+        (WI.ResourceContentView.prototype._handleRemoveLocalResourceOverride):
+        (WI.ResourceContentView.prototype._handleLocalResourceOverrideChanged):
+        (WI.ResourceContentView.prototype._mouseWasClicked):
+        * UserInterface/Views/TextResourceContentView.js:
+        (WI.TextResourceContentView):
+        (WI.TextResourceContentView.prototype.get navigationItems):
+        (WI.TextResourceContentView.prototype.localResourceOverrideInitialContent):
+        (WI.TextResourceContentView.prototype._contentWillPopulate):
+        (WI.TextResourceContentView.prototype._contentDidPopulate):
+        (WI.TextResourceContentView.prototype._textEditorContentDidChange):
+        (WI.TextResourceContentView.prototype._shouldBeEditable):
+        (WI.TextResourceContentView.prototype.async._handleCreateLocalResourceOverride): Deleted.
+        (WI.TextResourceContentView.prototype._handleRemoveLocalResourceOverride): Deleted.
+        (WI.TextResourceContentView.prototype._handleLocalResourceOverrideChanged): Deleted.
+        Extract generalized local resource override properties into the ResourceContentView base class.
+
+        * UserInterface/Views/FontResourceContentView.css:
+        (.content-view.resource.font):
+        (.content-view.resource.font > .drop-zone):
+        (.content-view.resource.font > .preview-container):
+        (.content-view.resource.font .preview):
+        * UserInterface/Views/FontResourceContentView.js:
+        (WI.FontResourceContentView):
+        (WI.FontResourceContentView.prototype.contentAvailable):
+        (WI.FontResourceContentView.prototype.shown):
+        (WI.FontResourceContentView.prototype.hidden):
+        (WI.FontResourceContentView.prototype.closed):
+        (WI.FontResourceContentView.prototype.layout):
+        (WI.FontResourceContentView.prototype._updatePreviewElement.createMetricElement):
+        (WI.FontResourceContentView.prototype._updatePreviewElement):
+        (WI.FontResourceContentView.prototype.dropZoneShouldAppearForDragEvent):
+        (WI.FontResourceContentView.prototype.dropZoneHandleDrop):
+        Create a drop zone that will update the font local resource override content.
+
+        * UserInterface/Views/ImageResourceContentView.css:
+        (.content-view.resource.image):
+        (.content-view.resource.image > .drop-zone):
+        (.content-view.resource.image > .img-container):
+        (.content-view.resource.image img):
+        * UserInterface/Views/ImageResourceContentView.js:
+        (WI.ImageResourceContentView):
+        (WI.ImageResourceContentView.prototype.get navigationItems):
+        (WI.ImageResourceContentView.prototype.contentAvailable):
+        (WI.ImageResourceContentView.prototype.closed):
+        (WI.ImageResourceContentView.prototype.dropZoneShouldAppearForDragEvent):
+        (WI.ImageResourceContentView.prototype.dropZoneHandleDrop):
+        Create a drop zone that will update the image local resource override content.
+
+        * UserInterface/Models/Script.js:
+        (WI.Script.prototype.get mimeType):
+        Seems like this should have a default value given there may not be a resource.
+
+        * UserInterface/Views/LocalResourceOverridePopover.js:
+        (WI.LocalResourceOverridePopover.prototype.show):
+        Better handling here, since the utilities expects a number not a string.
+
+        * UserInterface/Models/Resource.js:
+        (WI.Resource.prototype.createObjectURL):
+        * UserInterface/Views/LocalResourceOverrideTreeElement.js:
+        (WI.LocalResourceOverrideTreeElement.prototype.willDismissPopover):
+        Use currentRevision more appropriately.
+
+        * UserInterface/Models/SourceCode.js:
+        (WI.SourceCode.prototype._processContent):
+        * UserInterface/Views/TextResourceContentView.js:
+        (WI.TextResourceContentView.prototype._textEditorContentDidChange):
+        * UserInterface/Controllers/CSSManager.js:
+        (WI.CSSManager.prototype._resourceContentDidChange.applyStyleSheetChanges.styleSheetFound):
+        (WI.CSSManager.prototype._resourceContentDidChange.applyStyleSheetChanges):
+        (WI.CSSManager.prototype._resourceContentDidChange):
+        (WI.CSSManager.prototype._updateResourceContent.fetchedStyleSheetContent):
+        Update revision content more explicitly.
+
 2019-10-10  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: Sources: enable tab by default
diff --git a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
index 9e102f1..b6ae45f 100644
--- a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
+++ b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
@@ -379,6 +379,8 @@
 localizedStrings["Done"] = "Done";
 localizedStrings["Download"] = "Download";
 localizedStrings["Download Web Archive"] = "Download Web Archive";
+localizedStrings["Drop Font"] = "Drop Font";
+localizedStrings["Drop Image"] = "Drop Image";
 localizedStrings["Dropped Element"] = "Dropped Element";
 localizedStrings["Dropped Node"] = "Dropped Node";
 localizedStrings["Duplicate Selector"] = "Duplicate Selector";
diff --git a/Source/WebInspectorUI/UserInterface/Base/BlobUtilities.js b/Source/WebInspectorUI/UserInterface/Base/BlobUtilities.js
new file mode 100644
index 0000000..08c60b0
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/Base/BlobUtilities.js
@@ -0,0 +1,70 @@
+/*
+ * 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.BlobUtilities = class BlobUtilities {
+    static blobForContent(content, base64Encoded, mimeType)
+    {
+        if (base64Encoded)
+            return BlobUtilities.decodeBase64ToBlob(content, base64Encoded, mimeType);
+        return BlobUtilities.textToBlob(content, mimeType);
+    }
+
+    static decodeBase64ToBlob(base64Data, mimeType)
+    {
+        mimeType = mimeType || "";
+
+        const sliceSize = 1024;
+        let byteCharacters = atob(base64Data);
+        let bytesLength = byteCharacters.length;
+        let slicesCount = Math.ceil(bytesLength / sliceSize);
+        let byteArrays = new Array(slicesCount);
+
+        for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
+            let begin = sliceIndex * sliceSize;
+            let end = Math.min(begin + sliceSize, bytesLength);
+
+            let bytes = new Array(end - begin);
+            for (let offset = begin, i = 0; offset < end; ++i, ++offset)
+                bytes[i] = byteCharacters[offset].charCodeAt(0);
+
+            byteArrays[sliceIndex] = new Uint8Array(bytes);
+        }
+
+        return new Blob(byteArrays, {type: mimeType});
+    }
+
+    static textToBlob(text, mimeType)
+    {
+        return new Blob([text], {type: mimeType});
+    }
+
+    static blobAsText(blob, callback)
+    {
+        console.assert(blob instanceof Blob);
+        let fileReader = new FileReader;
+        fileReader.addEventListener("loadend", () => { callback(fileReader.result); });
+        fileReader.readAsText(blob);
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Base/MIMETypeUtilities.js b/Source/WebInspectorUI/UserInterface/Base/MIMETypeUtilities.js
index 37ef947..698838d 100644
--- a/Source/WebInspectorUI/UserInterface/Base/MIMETypeUtilities.js
+++ b/Source/WebInspectorUI/UserInterface/Base/MIMETypeUtilities.js
@@ -23,20 +23,25 @@
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WI.fileExtensionForURL = function(url)
+WI.fileExtensionForFilename = function(filename)
 {
-    let lastPathComponent = parseURL(url).lastPathComponent;
-    if (!lastPathComponent)
+    if (!filename)
         return null;
 
-    let index = lastPathComponent.lastIndexOf(".");
+    let index = filename.lastIndexOf(".");
     if (index === -1)
         return null;
 
-    if (index === lastPathComponent.length - 1)
+    if (index === filename.length - 1)
         return null;
 
-    return lastPathComponent.substr(index + 1);
+    return filename.substr(index + 1);
+};
+
+WI.fileExtensionForURL = function(url)
+{
+    let lastPathComponent = parseURL(url).lastPathComponent;
+    return WI.fileExtensionForFilename(lastPathComponent);
 };
 
 WI.mimeTypeForFileExtension = function(extension)
diff --git a/Source/WebInspectorUI/UserInterface/Base/Utilities.js b/Source/WebInspectorUI/UserInterface/Base/Utilities.js
index c3e139e..4d8ac05 100644
--- a/Source/WebInspectorUI/UserInterface/Base/Utilities.js
+++ b/Source/WebInspectorUI/UserInterface/Base/Utilities.js
@@ -1674,40 +1674,3 @@
 {
     array.splice(insertionIndexForObjectInListSortedByFunction(object, array, comparator), 0, object);
 }
-
-function decodeBase64ToBlob(base64Data, mimeType)
-{
-    mimeType = mimeType || "";
-
-    const sliceSize = 1024;
-    var byteCharacters = atob(base64Data);
-    var bytesLength = byteCharacters.length;
-    var slicesCount = Math.ceil(bytesLength / sliceSize);
-    var byteArrays = new Array(slicesCount);
-
-    for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
-        var begin = sliceIndex * sliceSize;
-        var end = Math.min(begin + sliceSize, bytesLength);
-
-        var bytes = new Array(end - begin);
-        for (var offset = begin, i = 0; offset < end; ++i, ++offset)
-            bytes[i] = byteCharacters[offset].charCodeAt(0);
-
-        byteArrays[sliceIndex] = new Uint8Array(bytes);
-    }
-
-    return new Blob(byteArrays, {type: mimeType});
-}
-
-function textToBlob(text, mimeType)
-{
-    return new Blob([text], {type: mimeType});
-}
-
-function blobAsText(blob, callback)
-{
-    console.assert(blob instanceof Blob);
-    let fileReader = new FileReader;
-    fileReader.addEventListener("loadend", () => { callback(fileReader.result); });
-    fileReader.readAsText(blob);
-}
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CSSManager.js b/Source/WebInspectorUI/UserInterface/Controllers/CSSManager.js
index 9c78ee4..0f8f20a 100644
--- a/Source/WebInspectorUI/UserInterface/Controllers/CSSManager.js
+++ b/Source/WebInspectorUI/UserInterface/Controllers/CSSManager.js
@@ -656,7 +656,7 @@
                 resource.__ignoreNextUpdateResourceContent = true;
 
                 let revision = styleSheet.currentRevision;
-                revision.content = resource.content;
+                revision.updateRevisionContent(resource.content);
             }
 
             this._lookupStyleSheetForResource(resource, styleSheetFound.bind(this));
@@ -701,10 +701,10 @@
 
             let revision = representedObject.currentRevision;
             if (styleSheet.isInspectorStyleSheet()) {
-                revision.content = representedObject.content;
+                revision.updateRevisionContent(representedObject.content);
                 styleSheet.dispatchEventToListeners(WI.SourceCode.Event.ContentDidChange);
             } else
-                revision.content = parameters.content;
+                revision.updateRevisionContent(parameters.content);
 
             this._ignoreResourceContentDidChangeEventForResource = null;
         }
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/NetworkManager.js b/Source/WebInspectorUI/UserInterface/Controllers/NetworkManager.js
index fa4f33c..1b536b4 100644
--- a/Source/WebInspectorUI/UserInterface/Controllers/NetworkManager.js
+++ b/Source/WebInspectorUI/UserInterface/Controllers/NetworkManager.js
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-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
@@ -816,9 +816,15 @@
         }
 
         let localResource = localResourceOverride.localResource;
-        let content = localResource.localContent;
-        let base64Encoded = localResource.localContentIsBase64Encoded;
-        let {mimeType, statusCode, statusText, responseHeaders} = localResource;
+        let revision = localResource.currentRevision;
+
+        let content = revision.content;
+        let base64Encoded = revision.base64Encoded;
+        let mimeType = revision.mimeType;
+        let statusCode = localResource.statusCode;
+        let statusText = localResource.statusText;
+        let responseHeaders = localResource.responseHeaders;
+        console.assert(revision.mimeType === localResource.mimeType);
 
         if (isNaN(statusCode))
             statusCode = undefined;
@@ -1244,17 +1250,6 @@
         if (!localResourceOverride)
             return;
 
-        let localResource = localResourceOverride.localResource;
-        let content = localResource.content;
-        let base64Encoded = localResource.localContentIsBase64Encoded;
-        let mimeType = localResource.mimeType;
-        localResource.updateOverrideContent(content, base64Encoded, mimeType, {suppressEvent: true});
-
-        this._persistLocalResourceOverrideSoonAfterContentChange(localResourceOverride);
-    }
-
-    _persistLocalResourceOverrideSoonAfterContentChange(localResourceOverride)
-    {
         if (!this._saveLocalResourceOverridesDebouncer) {
             this._pendingLocalResourceOverrideSaves = new Set;
             this._saveLocalResourceOverridesDebouncer = new Debouncer(() => {
diff --git a/Source/WebInspectorUI/UserInterface/Main.html b/Source/WebInspectorUI/UserInterface/Main.html
index 8a97915..9bb49b1 100644
--- a/Source/WebInspectorUI/UserInterface/Main.html
+++ b/Source/WebInspectorUI/UserInterface/Main.html
@@ -94,6 +94,7 @@
     <link rel="stylesheet" href="Views/DefaultDashboardView.css">
     <link rel="stylesheet" href="Views/DetailsSection.css">
     <link rel="stylesheet" href="Views/DividerNavigationItem.css">
+    <link rel="stylesheet" href="Views/DropZoneView.css">
     <link rel="stylesheet" href="Views/Editing.css">
     <link rel="stylesheet" href="Views/ErrorObjectView.css">
     <link rel="stylesheet" href="Views/EventBreakpointPopover.css">
@@ -290,6 +291,7 @@
     <script src="Base/Object.js"></script>
     <script src="Base/Throttler.js"></script>
 
+    <script src="Base/BlobUtilities.js"></script>
     <script src="Base/DOMUtilities.js"></script>
     <script src="Base/EventListener.js"></script>
     <script src="Base/EventListenerSet.js"></script>
@@ -660,6 +662,7 @@
     <script src="Views/DebuggerDashboardView.js"></script>
     <script src="Views/DefaultDashboardView.js"></script>
     <script src="Views/DividerNavigationItem.js"></script>
+    <script src="Views/DropZoneView.js"></script>
     <script src="Views/EditableDataGridNode.js"></script>
     <script src="Views/EditingSupport.js"></script>
     <script src="Views/ErrorObjectView.js"></script>
diff --git a/Source/WebInspectorUI/UserInterface/Models/LocalResource.js b/Source/WebInspectorUI/UserInterface/Models/LocalResource.js
index 666f0db..f51adb4 100644
--- a/Source/WebInspectorUI/UserInterface/Models/LocalResource.js
+++ b/Source/WebInspectorUI/UserInterface/Models/LocalResource.js
@@ -74,10 +74,6 @@
         this._responseBodyTransferSize = !isNaN(metrics.responseBodyBytesReceived) ? metrics.responseBodyBytesReceived : NaN;
         this._responseBodySize = !isNaN(metrics.responseBodyDecodedSize) ? metrics.responseBodyDecodedSize : NaN;
 
-        // Access to the content.
-        this._localContent = response.content;
-        this._localContentIsBase64Encoded = response.base64Encoded;
-
         // LocalResource specific.
         this._isLocalResourceOverride = isLocalResourceOverride || false;
 
@@ -87,7 +83,9 @@
         this._cached = false; // FIXME: How should we denote cached? Assume from response source?
 
         // Finalize WI.SourceCode.
-        this._originalRevision = new WI.SourceCodeRevision(this, this._localContent);
+        let content = response.content;
+        let base64Encoded = response.base64Encoded;
+        this._originalRevision = new WI.SourceCodeRevision(this, content, base64Encoded, this._mimeType);
         this._currentRevision = this._originalRevision;
     }
 
@@ -223,8 +221,8 @@
                 mimeType: this.mimeType,
                 statusCode: this.statusCode,
                 statusText: this.statusText,
-                content: this._localContent,
-                base64Encoded: this._localContentIsBase64Encoded,
+                content: this.currentRevision.content,
+                base64Encoded: this.currentRevision.base64Encoded,
             },
             isLocalResourceOverride: this._isLocalResourceOverride,
         };
@@ -232,58 +230,27 @@
 
     // Public
 
-    get localContent() { return this._localContent; }
-    get localContentIsBase64Encoded() { return this._localContentIsBase64Encoded; }
-
     get isLocalResourceOverride()
     {
         return this._isLocalResourceOverride;
     }
 
-    hasContent()
-    {
-        return !!this._localContent;
-    }
-
-    setContent(content, base64Encoded)
-    {
-        console.assert(!this._localContent);
-
-        // The backend may send base64 encoded data for text resources.
-        // If that is the case decode them here and treat as text.
-        if (base64Encoded && WI.shouldTreatMIMETypeAsText(this._mimeType)) {
-            content = atob(content);
-            base64Encoded = false;
-        }
-
-        this._localContent = content;
-        this._localContentIsBase64Encoded = base64Encoded;
-    }
-
-    updateOverrideContent(content, base64Encoded, mimeType, options = {})
-    {
-        console.assert(this._isLocalResourceOverride);
-
-        if (content !== undefined && this._localContent !== content)
-            this._localContent = content;
-
-        if (base64Encoded !== undefined && this._localContentIsBase64Encoded !== base64Encoded)
-            this._localContentIsBase64Encoded = base64Encoded;
-
-        if (mimeType !== undefined && mimeType !== this._mimeType) {
-            let oldMIMEType = this._mimeType;
-            this._mimeType = mimeType;
-            this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
-        }
-    }
-
     // Protected
 
     requestContentFromBackend()
     {
         return Promise.resolve({
-            content: this._localContent,
-            base64Encoded: this._localContentIsBase64Encoded,
+            content: this._originalRevision.content,
+            base64Encoded: this._originalRevision.base64Encoded,
         });
     }
+
+    handleCurrentRevisionContentChange()
+    {
+        if (this._mimeType !== this.currentRevision.mimeType) {
+            let oldMIMEType = this._mimeType;
+            this._mimeType = this.currentRevision.mimeType;
+            this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
+        }
+    }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Models/Resource.js b/Source/WebInspectorUI/UserInterface/Models/Resource.js
index 4feddc6..52aa0b8 100644
--- a/Source/WebInspectorUI/UserInterface/Models/Resource.js
+++ b/Source/WebInspectorUI/UserInterface/Models/Resource.js
@@ -422,21 +422,14 @@
 
     createObjectURL()
     {
+        let revision = this.currentRevision;
+        let blobContent = revision.blobContent;
+        if (blobContent)
+            return URL.createObjectURL(blobContent)
+
         // If content is not available, fallback to using original URL.
         // The client may try to revoke it, but nothing will happen.
-        let content = this.content;
-        if (!content)
-            return this._url;
-
-        if (content instanceof Blob)
-            return URL.createObjectURL(content);
-
-        if (typeof content === "string") {
-            let blob = textToBlob(content, this._mimeType);
-            return URL.createObjectURL(blob);
-        }
-
-        return null;
+        return this._url;
     }
 
     isMainResource()
@@ -1065,7 +1058,7 @@
         cookie[WI.Resource.MainResourceCookieKey] = this.isMainResource();
     }
 
-    async createLocalResourceOverride(initialContent)
+    async createLocalResourceOverride({initialContent} = {})
     {
         console.assert(!this.isLocalResourceOverride);
         console.assert(WI.NetworkManager.supportsLocalResourceOverrides());
diff --git a/Source/WebInspectorUI/UserInterface/Models/Script.js b/Source/WebInspectorUI/UserInterface/Models/Script.js
index fa298eb..967b565 100644
--- a/Source/WebInspectorUI/UserInterface/Models/Script.js
+++ b/Source/WebInspectorUI/UserInterface/Models/Script.js
@@ -117,7 +117,7 @@
 
     get mimeType()
     {
-        return this._resource.mimeType;
+        return this._resource ? this._resource.mimeType : "text/javascript";
     }
 
     get isScript()
diff --git a/Source/WebInspectorUI/UserInterface/Models/SourceCode.js b/Source/WebInspectorUI/UserInterface/Models/SourceCode.js
index 60278be..af78650 100644
--- a/Source/WebInspectorUI/UserInterface/Models/SourceCode.js
+++ b/Source/WebInspectorUI/UserInterface/Models/SourceCode.js
@@ -29,7 +29,7 @@
     {
         super();
 
-        this._originalRevision = new WI.SourceCodeRevision(this, null, false);
+        this._originalRevision = new WI.SourceCodeRevision(this);
         this._currentRevision = this._originalRevision;
 
         this._sourceMaps = null;
@@ -231,17 +231,22 @@
     {
         // Different backend APIs return one of `content, `body`, `text`, or `scriptSource`.
         let rawContent = parameters.content || parameters.body || parameters.text || parameters.scriptSource;
+        let rawBase64Encoded = !!parameters.base64Encoded;
         let content = rawContent;
         let error = parameters.error;
         let message = parameters.message;
 
         if (parameters.base64Encoded)
-            content = content ? decodeBase64ToBlob(content, this.mimeType) : "";
+            content = content ? WI.BlobUtilities.decodeBase64ToBlob(content, this.mimeType) : "";
 
         let revision = this.revisionForRequestedContent;
 
         this._ignoreRevisionContentDidChangeEvent = true;
-        revision.content = content || null;
+        revision.updateRevisionContent(rawContent, {
+            base64Encoded: rawBase64Encoded,
+            mimeType: this.mimeType,
+            blobContent: content instanceof Blob ? content : null,
+        });
         this._ignoreRevisionContentDidChangeEvent = false;
 
         this._initializeCurrentRevisionIfNeeded();
@@ -249,6 +254,8 @@
         // FIXME: Returning the content in this promise is misleading. It may not be current content
         // now, and it may become out-dated later on. We should drop content from this promise
         // and require clients to ask for the current contents from the sourceCode in the result.
+        // That would also avoid confusion around `content` being a Blob and eliminate the work
+        // of creating the Blob if it is not used.
 
         return Promise.resolve({
             error,
@@ -256,7 +263,7 @@
             sourceCode: this,
             content,
             rawContent,
-            rawBase64Encoded: parameters.base64Encoded,
+            rawBase64Encoded,
         });
     }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Models/SourceCodeRevision.js b/Source/WebInspectorUI/UserInterface/Models/SourceCodeRevision.js
index bda3e0f..7a52cde 100644
--- a/Source/WebInspectorUI/UserInterface/Models/SourceCodeRevision.js
+++ b/Source/WebInspectorUI/UserInterface/Models/SourceCodeRevision.js
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-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
@@ -25,36 +25,56 @@
 
 WI.SourceCodeRevision = class SourceCodeRevision extends WI.Revision
 {
-    constructor(sourceCode, content)
+    constructor(sourceCode, content, base64Encoded, mimeType)
     {
         super();
 
         console.assert(sourceCode instanceof WI.SourceCode);
+        console.assert(content === undefined || typeof content === "string");
+        console.assert(base64Encoded === undefined || typeof base64Encoded === "boolean");
+        console.assert(mimeType === undefined || typeof mimeType === "string");
 
         this._sourceCode = sourceCode;
+
         this._content = content || "";
+        this._base64Encoded = !!base64Encoded;
+        this._mimeType = mimeType;
+        this._blobContent = null;
     }
 
     // Public
 
-    get sourceCode()
+    get sourceCode() { return this._sourceCode; }
+    get content() { return this._content; }
+    get base64Encoded() { return this._base64Encoded; }
+    get mimeType() { return this._mimeType; }
+
+    get blobContent()
     {
-        return this._sourceCode;
+        if (!this._blobContent && this._content)
+            this._blobContent = WI.BlobUtilities.blobForContent(this._content, this._base64Encoded, this._mimeType);
+
+        console.assert(!this._blobContent || this._blobContent instanceof Blob);
+        return this._blobContent;
     }
 
-    get content()
+    updateRevisionContent(content, {base64Encoded, mimeType, blobContent} = {})
     {
-        return this._content;
-    }
+        console.assert(content === undefined || typeof content === "string");
+        this._content = content || "";
 
-    set content(content)
-    {
-        content = content || "";
+        if (base64Encoded !== undefined) {
+            console.assert(typeof base64Encoded === "boolean");
+            this._base64Encoded = !!base64Encoded;
+        }
 
-        if (this._content === content)
-            return;
+        if (mimeType !== undefined) {
+            console.assert(typeof mimeType === "string");
+            this._mimeType = mimeType;
+        }
 
-        this._content = content;
+        console.assert(!blobContent || blobContent instanceof Blob);
+        this._blobContent = blobContent !== undefined ? blobContent : null;
 
         this._sourceCode.revisionContentDidChange(this);
     }
@@ -71,6 +91,6 @@
 
     copy()
     {
-        return new WI.SourceCodeRevision(this._sourceCode, this._content);
+        return new WI.SourceCodeRevision(this._sourceCode, this._content, this._base64Encoded, this._mimeType);
     }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Test.html b/Source/WebInspectorUI/UserInterface/Test.html
index ad4765c..660ece2 100644
--- a/Source/WebInspectorUI/UserInterface/Test.html
+++ b/Source/WebInspectorUI/UserInterface/Test.html
@@ -54,6 +54,7 @@
     <script src="Test/TestAppController.js"></script>
     <script src="Test/TestUtilities.js"></script>
 
+    <script src="Base/BlobUtilities.js"></script>
     <script src="Base/DOMUtilities.js"></script>
     <script src="Base/EventListener.js"></script>
     <script src="Base/EventListenerSet.js"></script>
diff --git a/Source/WebInspectorUI/UserInterface/Views/DropZoneView.css b/Source/WebInspectorUI/UserInterface/Views/DropZoneView.css
new file mode 100644
index 0000000..ec7e1cf
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/Views/DropZoneView.css
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+.drop-zone {
+    display: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: var(--z-index-glass-pane-for-drag);
+}
+
+.drop-zone.visible {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 60px;
+    text-align: center;
+    color: white;
+    text-shadow: 0 1px black;
+    background-color: hsla(0, 0%, 50%, 0.50);
+    border: 3px dashed hsla(0, 0%, 100%, 0.9);
+    -webkit-backdrop-filter: blur(10px);
+}
+
+@media (prefers-color-scheme: dark) {
+    .drop-zone.visible {
+        color: hsl(0, 0%, 80%);
+        text-shadow: 0 1px hsl(0, 0%, 20%);
+        background-color: hsla(0, 0%, 30%, 0.5);
+        border: 3px dashed hsla(0, 0%, 65%, 0.9);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/DropZoneView.js b/Source/WebInspectorUI/UserInterface/Views/DropZoneView.js
new file mode 100644
index 0000000..ea35d7d
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/Views/DropZoneView.js
@@ -0,0 +1,155 @@
+/*
+ * 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.
+ */
+
+// DropZoneView creates an invisible drop zone when a drag enters a target element.
+// There are delegate methods for deciding if the drop zone should appear, drag
+// progress such as entering and leaving, and the drop itself. Clients should
+// always initialize with a delegate and set a target element before showing.
+
+WI.DropZoneView = class DropZoneView extends WI.View
+{
+    constructor(delegate, {text} = {})
+    {
+        console.assert(delegate);
+        console.assert(typeof delegate.dropZoneShouldAppearForDragEvent === "function");
+
+        super();
+
+        this._delegate = delegate;
+        this._targetElement = null;
+        this._activelyHandlingDrag = false;
+
+        if (text)
+            this.element.textContent = text;
+
+        this.element.classList.add("drop-zone");
+    }
+
+    // Public
+
+    get delegate() { return this._delegate; }
+
+    get targetElement()
+    {
+        return this._targetElement;
+    }
+
+    set targetElement(element)
+    {
+        console.assert(!this._activelyHandlingDrag);
+
+        if (this._targetElement === element)
+            return;
+
+        if (!this._boundHandleDragEnter)
+            this._boundHandleDragEnter = this._handleDragEnter.bind(this);
+
+        if (this._targetElement)
+            this._targetElement.removeEventListener("dragenter", this._boundHandleDragEnter);
+
+        this._targetElement = element;
+
+        if (this._targetElement)
+            this._targetElement.addEventListener("dragenter", this._boundHandleDragEnter);
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        console.assert(this._targetElement);
+
+        this.element.addEventListener("dragover", this._handleDragOver.bind(this));
+        this.element.addEventListener("dragleave", this._handleDragLeave.bind(this));
+        this.element.addEventListener("drop", this._handleDrop.bind(this));
+    }
+
+    // Private
+
+    _startActiveDrag()
+    {
+        console.assert(!this._activelyHandlingDrag);
+        this._activelyHandlingDrag = true;
+        this.element.classList.add("visible");
+    }
+
+    _stopActiveDrag()
+    {
+        console.assert(this._activelyHandlingDrag);
+        this._activelyHandlingDrag = false;
+        this.element.classList.remove("visible");
+    }
+
+    _handleDragEnter(event)
+    {
+        console.assert(this.isAttached);
+        if (this._activelyHandlingDrag)
+            return;
+
+        if (!this._delegate.dropZoneShouldAppearForDragEvent(this, event))
+            return;
+
+        this._startActiveDrag();
+
+        if (this._delegate.dropZoneHandleDragEnter)
+            this._delegate.dropZoneHandleDragEnter(this, event);
+    }
+
+    _handleDragLeave(event)
+    {
+        if (!this._activelyHandlingDrag)
+            return;
+
+        this._stopActiveDrag();
+
+        if (this._delegate.dropZoneHandleDragLeave)
+            this._delegate.dropZoneHandleDragLeave(this, event);
+    }
+
+    _handleDragOver(event)
+    {
+        if (!this._activelyHandlingDrag)
+            return;
+
+        event.preventDefault();
+
+        event.dataTransfer.dropEffect = "copy";
+    }
+
+    _handleDrop(event)
+    {
+        if (!this._activelyHandlingDrag)
+            return;
+
+        event.preventDefault();
+
+        this._stopActiveDrag();
+
+        if (this._delegate.dropZoneHandleDrop)
+            this._delegate.dropZoneHandleDrop(this, event);
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.css b/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.css
index 29661a3..18d49f2 100644
--- a/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.css
+++ b/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.css
@@ -25,13 +25,25 @@
 
 .content-view.resource.font {
     display: flex;
-
+    flex-direction: column;
     justify-content: center;
-
+    align-items: center;
     overflow-x: hidden;
     overflow-y: auto;
 }
 
+.content-view.resource.font > .drop-zone {
+    top: calc(var(--navigation-bar-height) - 2px); /* borders */
+}
+
+.content-view.resource.font > .preview-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+}
+
 .content-view.resource.font .preview {
     font-size: 72px;
     font-family: serif;
@@ -42,8 +54,6 @@
     overflow there will be space on all sides. It also avoids having to account
     for the padding/margin in FontResourceContentView.siteToFit. */
     border: 15px solid transparent;
-
-    margin: auto 0;
 }
 
 .content-view.resource.font .preview > .line {
diff --git a/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js b/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js
index 9315a5c..112d98f 100644
--- a/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/FontResourceContentView.js
@@ -31,15 +31,14 @@
 
         this._styleElement = null;
         this._previewElement = null;
+        this._previewContainer = null;
+
+        if (this.showingLocalResourceOverride)
+            this._dropZoneView = new WI.DropZoneView(this, {text: WI.UIString("Drop Font")});
     }
 
     // Public
 
-    get previewElement()
-    {
-        return this._previewElement;
-    }
-
     sizeToFit()
     {
         if (!this._previewElement)
@@ -56,68 +55,29 @@
     contentAvailable(content, base64Encoded)
     {
         this._fontObjectURL = this.resource.createObjectURL();
+
         if (!this._fontObjectURL) {
             this.showGenericErrorMessage();
             return;
         }
 
-        const uniqueFontName = "WebInspectorFontPreview" + (++WI.FontResourceContentView._uniqueFontIdentifier);
-
-        var format = "";
-
-        // We need to specify a format when loading SVG fonts to make them work.
-        if (this.resource.mimeTypeComponents.type === "image/svg+xml")
-            format = " format(\"svg\")";
-
-        if (this._styleElement && this._styleElement.parentNode)
-            this._styleElement.parentNode.removeChild(this._styleElement);
-
         this.removeLoadingIndicator();
 
-        this._styleElement = document.createElement("style");
-        this._styleElement.textContent = "@font-face { font-family: \"" + uniqueFontName + "\"; src: url(" + this._fontObjectURL + ")" + format + "; }";
+        this._previewContainer = this.element.appendChild(document.createElement("div"));
+        this._previewContainer.className = "preview-container";
 
-        // The style element will be added when shown later if we are not visible now.
-        if (this.visible)
-            document.head.appendChild(this._styleElement);
+        this._updatePreviewElement();
 
-        this._previewElement = document.createElement("div");
-        this._previewElement.className = "preview";
-        this._previewElement.style.fontFamily = uniqueFontName;
-
-        function createMetricElement(className)
-        {
-            var metricElement = document.createElement("div");
-            metricElement.className = "metric " + className;
-            return metricElement;
+        if (this._dropZoneView) {
+            this._dropZoneView.targetElement = this._previewContainer;
+            this.addSubview(this._dropZoneView);
         }
-
-        var lines = WI.FontResourceContentView.PreviewLines;
-        for (var i = 0; i < lines.length; ++i) {
-            var lineElement = document.createElement("div");
-            lineElement.className = "line";
-
-            lineElement.appendChild(createMetricElement("top"));
-            lineElement.appendChild(createMetricElement("xheight"));
-            lineElement.appendChild(createMetricElement("middle"));
-            lineElement.appendChild(createMetricElement("baseline"));
-            lineElement.appendChild(createMetricElement("bottom"));
-
-            var contentElement = document.createElement("div");
-            contentElement.className = "content";
-            contentElement.textContent = lines[i];
-            lineElement.appendChild(contentElement);
-
-            this._previewElement.appendChild(lineElement);
-        }
-
-        this.element.appendChild(this._previewElement);
-
-        this.sizeToFit();
     }
 
     shown()
     {
+        super.shown();
+
         // Add the style element since it is removed when hidden.
         if (this._styleElement)
             document.head.appendChild(this._styleElement);
@@ -126,8 +86,10 @@
     hidden()
     {
         // Remove the style element so it will not stick around when this content view is destroyed.
-        if (this._styleElement && this._styleElement.parentNode)
-            this._styleElement.parentNode.removeChild(this._styleElement);
+        if (this._styleElement)
+            this._styleElement.remove();
+
+        super.hidden();
     }
 
     closed()
@@ -138,6 +100,8 @@
         // the object URL to be resolved again.
         if (this._fontObjectURL)
             URL.revokeObjectURL(this._fontObjectURL);
+
+        super.closed();
     }
 
     // Protected
@@ -146,6 +110,104 @@
     {
         this.sizeToFit();
     }
+
+    // DropZoneView delegate
+
+    dropZoneShouldAppearForDragEvent(dropZone, event)
+    {
+        return event.dataTransfer.types.includes("Files");
+    }
+
+    dropZoneHandleDrop(dropZone, event)
+    {
+        let files = event.dataTransfer.files;
+        let file = files.length === 1 ? files[0] : null;
+        if (!file) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        let fileReader = new FileReader;
+        fileReader.addEventListener("loadend", (event) => {
+            let localResourceOverride = WI.networkManager.localResourceOverrideForURL(this.resource.url);
+            if (!localResourceOverride)
+                return;
+
+            let dataURL = fileReader.result;
+            let {base64, data, mimeType} = parseDataURL(dataURL);
+
+            // In case no mime type was determined, try to derive one from the file extension.
+            if (!mimeType || mimeType === "text/plain") {
+                let extension = WI.fileExtensionForFilename(file.name);
+                if (extension)
+                    mimeType = WI.mimeTypeForFileExtension(extension);
+            }
+
+            let revision = localResourceOverride.localResource.currentRevision;
+            revision.updateRevisionContent(data, {base64Encoded: base64, mimeType});
+
+            this._fontObjectURL = this.resource.createObjectURL();
+            this._updatePreviewElement();
+        });
+        fileReader.readAsDataURL(file);
+    }
+
+    // Private
+
+    _updatePreviewElement()
+    {
+        if (this._styleElement)
+            this._styleElement.remove();
+        if (this._previewElement)
+            this._previewElement.remove();
+
+        const uniqueFontName = "WebInspectorFontPreview" + (++WI.FontResourceContentView._uniqueFontIdentifier);
+
+        let format = "";
+
+        // We need to specify a format when loading SVG fonts to make them work.
+        if (this.resource.mimeTypeComponents.type === "image/svg+xml")
+            format = " format(\"svg\")";
+
+        this._styleElement = document.createElement("style");
+        this._styleElement.textContent = `@font-face { font-family: "${uniqueFontName}"; src: url(${this._fontObjectURL}) ${format}; }`;
+
+        // The style element will be added when shown later if we are not visible now.
+        if (this.visible)
+            document.head.appendChild(this._styleElement);
+
+        this._previewElement = document.createElement("div");
+        this._previewElement.className = "preview";
+        this._previewElement.style.fontFamily = uniqueFontName;
+
+        function createMetricElement(className) {
+            let metricElement = document.createElement("div");
+            metricElement.className = "metric " + className;
+            return metricElement;
+        }
+
+        for (let line of WI.FontResourceContentView.PreviewLines) {
+            let lineElement = document.createElement("div");
+            lineElement.className = "line";
+
+            lineElement.appendChild(createMetricElement("top"));
+            lineElement.appendChild(createMetricElement("xheight"));
+            lineElement.appendChild(createMetricElement("middle"));
+            lineElement.appendChild(createMetricElement("baseline"));
+            lineElement.appendChild(createMetricElement("bottom"));
+
+            let contentElement = document.createElement("div");
+            contentElement.className = "content";
+            contentElement.textContent = line;
+            lineElement.appendChild(contentElement);
+
+            this._previewElement.appendChild(lineElement);
+        }
+
+        this._previewContainer.appendChild(this._previewElement);
+
+        this.sizeToFit();
+    }
 };
 
 WI.FontResourceContentView._uniqueFontIdentifier = 0;
diff --git a/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.css b/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.css
index 9e6bc04..5c401a18 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.css
+++ b/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.css
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-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
@@ -24,26 +24,33 @@
  */
 
 .content-view.resource.image {
-    background-color: hsl(0, 0%, 90%);
-
-    overflow-x: hidden;
-    overflow-y: auto;
-
     display: flex;
-
+    flex-direction: column;
     justify-content: center;
     align-items: center;
+    background-color: hsl(0, 0%, 90%);
+    overflow-x: hidden;
+    overflow-y: auto;
+}
 
+.content-view.resource.image > .drop-zone {
+    top: calc(var(--navigation-bar-height) - 2px); /* borders */
+}
+
+.content-view.resource.image > .img-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
     padding: 15px;
 }
 
 .content-view.resource.image img {
     max-width: 100%;
-
+    max-height: 100%;
     -webkit-user-select: text;
     -webkit-user-drag: auto;
-
-    margin: auto 0;
 }
 
 @media (prefers-color-scheme: dark) {
diff --git a/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js b/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js
index ba94214..58f499b 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/ImageResourceContentView.js
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2015 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-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
@@ -27,9 +27,12 @@
 {
     constructor(resource)
     {
+        console.assert(resource instanceof WI.Resource);
+
         super(resource, "image");
 
         this._imageElement = null;
+        this._draggingInternalImageElement = false;
 
         const toolTip = WI.UIString("Show transparency grid");
         const activatedToolTip = WI.UIString("Hide transparency grid");
@@ -37,13 +40,20 @@
         this._showGridButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
         this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
         this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
+
+        if (this.showingLocalResourceOverride)
+            this._dropZoneView = new WI.DropZoneView(this, {text: WI.UIString("Drop Image")});
     }
 
     // Public
 
     get navigationItems()
     {
-        return [this._showGridButtonNavigationItem];
+        let items = super.navigationItems;
+
+        items.push(this._showGridButtonNavigationItem);
+
+        return items;
     }
 
     contentAvailable(content, base64Encoded)
@@ -61,13 +71,28 @@
             return;
         }
 
-        this._imageElement = document.createElement("img");
+        let imageContainer = this.element.appendChild(document.createElement("div"));
+        imageContainer.className = "img-container";
+
+        this._imageElement = imageContainer.appendChild(document.createElement("img"));
         this._imageElement.addEventListener("load", function() { URL.revokeObjectURL(objectURL); });
         this._imageElement.src = objectURL;
         this._imageElement.setAttribute("filename", this.resource.urlComponents.lastPathComponent || "");
         this._updateImageGrid();
 
-        this.element.appendChild(this._imageElement);
+        this._imageElement.addEventListener("dragstart", (event) => {
+            console.assert(!this._draggingInternalImageElement);
+            this._draggingInternalImageElement = true;
+        });
+        this._imageElement.addEventListener("dragend", (event) => {
+            console.assert(this._draggingInternalImageElement);
+            this._draggingInternalImageElement = false;
+        });
+
+        if (this._dropZoneView) {
+            this._dropZoneView.targetElement = imageContainer;
+            this.addSubview(this._dropZoneView);
+        }
     }
 
     // Protected
@@ -88,6 +113,58 @@
         super.hidden();
     }
 
+    closed()
+    {
+        WI.networkManager.removeEventListener(null, null, this);
+
+        super.closed();
+    }
+
+    // DropZoneView delegate
+
+    dropZoneShouldAppearForDragEvent(dropZone, event)
+    {
+        // Do not appear if the drag is the current image inside this view.
+        if (this._draggingInternalImageElement)
+            return false;
+
+        // Appear if the drop contains a file.
+        return event.dataTransfer.types.includes("Files");
+    }
+
+    dropZoneHandleDrop(dropZone, event)
+    {
+        let files = event.dataTransfer.files;
+        let file = files.length === 1 ? files[0] : null;
+        if (!file) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        let fileReader = new FileReader;
+        fileReader.addEventListener("loadend", (event) => {
+            let localResourceOverride = WI.networkManager.localResourceOverrideForURL(this.resource.url);
+            if (!localResourceOverride)
+                return;
+
+            let dataURL = fileReader.result;
+            this._imageElement.src = dataURL;
+
+            let {base64, data, mimeType} = parseDataURL(dataURL);
+
+            // In case no mime type was determined, try to derive one from the file extension.
+            if (!mimeType || mimeType === "text/plain") {
+                let extension = WI.fileExtensionForFilename(file.name);
+                if (extension)
+                    mimeType = WI.mimeTypeForFileExtension(extension);
+            }
+
+            let revision = localResourceOverride.localResource.currentRevision;
+            revision.updateRevisionContent(data, {base64Encoded: base64, mimeType});
+        });
+        fileReader.readAsDataURL(file);
+    }
+
     // Private
 
     _updateImageGrid()
diff --git a/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverridePopover.js b/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverridePopover.js
index ec0374e..c6101e4 100644
--- a/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverridePopover.js
+++ b/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverridePopover.js
@@ -118,7 +118,7 @@
         if (!statusCode || statusCode === "NaN")
             statusCode = "200";
         if (!statusText)
-            statusText = WI.HTTPUtilities.statusTextForStatusCode(statusCode);
+            statusText = WI.HTTPUtilities.statusTextForStatusCode(parseInt(statusCode));
 
         let popoverContentElement = document.createElement("div");
         popoverContentElement.className = "local-resource-override-popover-content";
diff --git a/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverrideTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverrideTreeElement.js
index 046c8e8..2f9dbc7 100644
--- a/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverrideTreeElement.js
+++ b/Source/WebInspectorUI/UserInterface/Views/LocalResourceOverrideTreeElement.js
@@ -125,14 +125,15 @@
 
         let wasSelected = this.selected;
 
+        let revision = this._localResourceOverride.localResource.currentRevision;
         let newLocalResourceOverride = WI.LocalResourceOverride.create({
             url,
             mimeType,
             statusCode,
             statusText,
             headers,
-            content: this._localResourceOverride.localResource.localContent,
-            base64Encoded: this._localResourceOverride.localResource.localContentIsBase64Encoded,
+            content: revision.content,
+            base64Encoded: revision.base64Encoded,
         });
 
         WI.networkManager.removeLocalResourceOverride(this._localResourceOverride);
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js b/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js
index 9bb31b3..82b361b 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/ResourceContentView.js
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-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
@@ -57,13 +57,48 @@
             for (var i = 0; i < issues.length; ++i)
                 this.addIssue(issues[i]);
         }
+
+        this._showingLocalResourceOverride = false;
+
+        if (WI.NetworkManager.supportsLocalResourceOverrides()) {
+            if (resource.isLocalResourceOverride) {
+                this._showingLocalResourceOverride = true;
+
+                this._localResourceOverrideBannerView = new WI.LocalResourceOverrideLabelView(resource);
+
+                this._removeLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("remove-local-resource-override", WI.UIString("Remove Local Override"), "Images/NavigationItemTrash.svg", 15, 15);
+                this._removeLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleRemoveLocalResourceOverride, this);
+                this._removeLocalResourceOverrideButtonNavigationItem.enabled = true;
+                this._removeLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+            } else {
+                this._localResourceOverrideBannerView = new WI.LocalResourceOverrideWarningView(resource);
+
+                this._createLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("create-local-resource-override", WI.UIString("Create Local Override"), "Images/NavigationItemNetworkOverride.svg", 13, 14);
+                this._createLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCreateLocalResourceOverride, this);
+                this._createLocalResourceOverrideButtonNavigationItem.enabled = false; // Enabled when the content is available.
+                this._createLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+            }
+
+            WI.networkManager.addEventListener(WI.NetworkManager.Event.LocalResourceOverrideAdded, this._handleLocalResourceOverrideChanged, this);
+            WI.networkManager.addEventListener(WI.NetworkManager.Event.LocalResourceOverrideRemoved, this._handleLocalResourceOverrideChanged, this);
+        }
     }
 
     // Public
 
-    get resource()
+    get resource() { return this._resource; }
+    get showingLocalResourceOverride() { return this._showingLocalResourceOverride; }
+
+    get navigationItems()
     {
-        return this._resource;
+        let items = [];
+
+        if (this._removeLocalResourceOverrideButtonNavigationItem)
+            items.push(this._removeLocalResourceOverrideButtonNavigationItem);
+        if (this._createLocalResourceOverrideButtonNavigationItem)
+            items.push(this._createLocalResourceOverrideButtonNavigationItem);
+
+        return items;
     }
 
     get supportsSave()
@@ -81,6 +116,12 @@
         throw WI.NotImplementedError.subclassMustOverride();
     }
 
+    localResourceOverrideInitialContent()
+    {
+        // Implemented by subclasses if needed.
+        return {};
+    }
+
     showGenericNoContentMessage()
     {
         this.showMessage(WI.UIString("Resource has no content"));
@@ -110,6 +151,9 @@
     {
         super.closed();
 
+        if (WI.NetworkManager.supportsLocalResourceOverrides())
+            WI.networkManager.removeEventListener(null, null, this);
+
         if (!this.managesOwnIssues)
             WI.consoleManager.removeEventListener(null, null, this);
     }
@@ -123,7 +167,10 @@
             this._spinnerTimeout = undefined;
         }
 
-        this.element.removeChildren();
+        this.removeAllSubviews();
+
+        if (this._localResourceOverrideBannerView)
+            this.addSubview(this._localResourceOverrideBannerView);
     }
 
     // Private
@@ -144,6 +191,9 @@
         console.assert(!this._hasContent());
         console.assert(parameters.sourceCode === this._resource);
         this.contentAvailable(parameters.sourceCode.content, parameters.base64Encoded);
+
+        if (this._createLocalResourceOverrideButtonNavigationItem)
+            this._createLocalResourceOverrideButtonNavigationItem.enabled = WI.networkManager.canBeOverridden(this._resource);
     }
 
     _contentError(error)
@@ -168,15 +218,40 @@
         console.assert(!this.managesOwnIssues);
 
         var issue = event.data.issue;
-        if (!WI.ConsoleManager.issueMatchSourceCode(issue, this.resource))
+        if (!WI.ConsoleManager.issueMatchSourceCode(issue, this._resource))
             return;
 
         this.addIssue(issue);
     }
 
+    async _handleCreateLocalResourceOverride(event)
+    {
+        let initialContent = this.localResourceOverrideInitialContent();
+        let localResourceOverride = await this._resource.createLocalResourceOverride(initialContent);
+        WI.networkManager.addLocalResourceOverride(localResourceOverride);
+        WI.showLocalResourceOverride(localResourceOverride);
+    }
+
+    _handleRemoveLocalResourceOverride(event)
+    {
+        console.assert(this._showingLocalResourceOverride);
+
+        let localResourceOverride = WI.networkManager.localResourceOverrideForURL(this._resource.url);
+        WI.networkManager.removeLocalResourceOverride(localResourceOverride);
+    }
+
+    _handleLocalResourceOverrideChanged(event)
+    {
+        if (this._resource.url !== event.data.localResourceOverride.url)
+            return;
+
+        if (this._createLocalResourceOverrideButtonNavigationItem)
+            this._createLocalResourceOverrideButtonNavigationItem.enabled = WI.networkManager.canBeOverridden(this._resource);
+    }
+
     _mouseWasClicked(event)
     {
-        WI.handlePossibleLinkClick(event, this.resource.parentFrame);
+        WI.handlePossibleLinkClick(event, this._resource.parentFrame);
     }
 };
 
diff --git a/Source/WebInspectorUI/UserInterface/Views/TextResourceContentView.js b/Source/WebInspectorUI/UserInterface/Views/TextResourceContentView.js
index 77073db..dec2408 100644
--- a/Source/WebInspectorUI/UserInterface/Views/TextResourceContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/TextResourceContentView.js
@@ -56,27 +56,6 @@
         this._codeCoverageButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
         WI.settings.enableControlFlowProfiler.addEventListener(WI.Setting.Event.Changed, this._enableControlFlowProfilerSettingChanged, this);
 
-        this._showingLocalResourceOverride = false;
-
-        if (WI.NetworkManager.supportsLocalResourceOverrides()) {
-            if (resource instanceof WI.Resource && resource.isLocalResourceOverride) {
-                this._showingLocalResourceOverride = true;
-                this._localResourceOverrideBannerView = new WI.LocalResourceOverrideLabelView(resource);
-
-                this._removeLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("remove-local-resource-override", WI.UIString("Remove Local Override"), "Images/NavigationItemTrash.svg", 15, 15);
-                this._removeLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleRemoveLocalResourceOverride, this);
-                this._removeLocalResourceOverrideButtonNavigationItem.enabled = true;
-                this._removeLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
-            } else {
-                this._localResourceOverrideBannerView = new WI.LocalResourceOverrideWarningView(resource);
-
-                this._createLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("create-local-resource-override", WI.UIString("Create Local Override"), "Images/NavigationItemNetworkOverride.svg", 13, 14);
-                this._createLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCreateLocalResourceOverride, this);
-                this._createLocalResourceOverrideButtonNavigationItem.enabled = false; // Enabled when the text editor is populated with content.
-                this._createLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
-            }
-        }
-
         this._textEditor = new WI.SourceCodeTextEditor(resource);
         this._textEditor.addEventListener(WI.TextEditor.Event.ExecutionLineNumberDidChange, this._executionLineNumberDidChange, this);
         this._textEditor.addEventListener(WI.TextEditor.Event.NumberOfSearchResultsDidChange, this._numberOfSearchResultsDidChange, this);
@@ -89,27 +68,17 @@
 
         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ProbeSetAdded, this._probeSetsChanged, this);
         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ProbeSetRemoved, this._probeSetsChanged, this);
-
-        if (WI.NetworkManager.supportsLocalResourceOverrides()) {
-            WI.networkManager.addEventListener(WI.NetworkManager.Event.LocalResourceOverrideAdded, this._handleLocalResourceOverrideChanged, this);
-            WI.networkManager.addEventListener(WI.NetworkManager.Event.LocalResourceOverrideRemoved, this._handleLocalResourceOverrideChanged, this);
-        }
     }
 
     // Public
 
     get navigationItems()
     {
-        let items = [];
-
-        if (this._removeLocalResourceOverrideButtonNavigationItem)
-            items.push(this._removeLocalResourceOverrideButtonNavigationItem);
-        if (this._createLocalResourceOverrideButtonNavigationItem)
-            items.push(this._createLocalResourceOverrideButtonNavigationItem);
+        let items = super.navigationItems;
 
         items.push(this._prettyPrintButtonNavigationItem);
 
-        if (!this._showingLocalResourceOverride)
+        if (!this.showingLocalResourceOverride)
             items.push(this._showTypesButtonNavigationItem, this._codeCoverageButtonNavigationItem);
 
         return items;
@@ -175,6 +144,11 @@
         // Do nothing.
     }
 
+    localResourceOverrideInitialContent()
+    {
+        return {initialContent: this._textEditor.string};
+    }
+
     get supportsSave()
     {
         return super.supportsSave || this.resource instanceof WI.CSSStyleSheet;
@@ -243,17 +217,11 @@
 
         this.removeLoadingIndicator();
 
-        if (this._localResourceOverrideBannerView)
-            this.addSubview(this._localResourceOverrideBannerView);
-
         this.addSubview(this._textEditor);
     }
 
     _contentDidPopulate(event)
     {
-        if (this._createLocalResourceOverrideButtonNavigationItem)
-            this._createLocalResourceOverrideButtonNavigationItem.enabled = WI.networkManager.canBeOverridden(this.resource);
-
         this._prettyPrintButtonNavigationItem.enabled = this._textEditor.canBeFormatted();
 
         this._showTypesButtonNavigationItem.enabled = this._textEditor.canShowTypeAnnotations();
@@ -263,21 +231,6 @@
         this._codeCoverageButtonNavigationItem.activated = WI.settings.enableControlFlowProfiler.value;
     }
 
-    async _handleCreateLocalResourceOverride(event)
-    {
-        let localResourceOverride = await this.resource.createLocalResourceOverride(this._textEditor.string);
-        WI.networkManager.addLocalResourceOverride(localResourceOverride);
-        WI.showLocalResourceOverride(localResourceOverride);
-    }
-
-    _handleRemoveLocalResourceOverride(event)
-    {
-        console.assert(this._showingLocalResourceOverride);
-
-        let localResourceOverride = WI.networkManager.localResourceOverrideForURL(this.resource.url);
-        WI.networkManager.removeLocalResourceOverride(localResourceOverride);
-    }
-
     _togglePrettyPrint(event)
     {
         var activated = !this._prettyPrintButtonNavigationItem.activated;
@@ -320,15 +273,6 @@
         this._prettyPrintButtonNavigationItem.enabled = this._textEditor.canBeFormatted();
     }
 
-    _handleLocalResourceOverrideChanged(event)
-    {
-        if (this.resource.url !== event.data.localResourceOverride.url)
-            return;
-
-        if (this._createLocalResourceOverrideButtonNavigationItem)
-            this._createLocalResourceOverrideButtonNavigationItem.enabled = WI.networkManager.canBeOverridden(this.resource);
-    }
-
     _sourceCodeContentDidChange(event)
     {
         if (this._ignoreSourceCodeContentDidChangeEvent)
@@ -340,7 +284,7 @@
     _textEditorContentDidChange(event)
     {
         this._ignoreSourceCodeContentDidChangeEvent = true;
-        this.resource.currentRevision.content = this._textEditor.string;
+        this.resource.currentRevision.updateRevisionContent(this._textEditor.string);
         this._ignoreSourceCodeContentDidChangeEvent = false;
     }
 
@@ -374,7 +318,7 @@
         if (this.resource.urlComponents.scheme === "file")
             return true;
 
-        if (this._showingLocalResourceOverride)
+        if (this.showingLocalResourceOverride)
             return true;
 
         return false;