diff --git a/LayoutTests/inspector/unit-tests/set-utilities-expected.txt b/LayoutTests/inspector/unit-tests/set-utilities-expected.txt index d2169d125e9f..d49b9293f180 100644 --- a/LayoutTests/inspector/unit-tests/set-utilities-expected.txt +++ b/LayoutTests/inspector/unit-tests/set-utilities-expected.txt @@ -12,6 +12,13 @@ PASS: Set can filter based on the set object. PASS: Set can filter with a different this. PASS: Set can filter based on the set object with a different this. +-- Running test case: Set.prototype.some +PASS: Set can 'some' based on the value. +PASS: Set can 'some' based on the key. +PASS: Set can 'some' based on the set object. +PASS: Set can 'some' with a different this. +PASS: Set can 'some' based on the set object with a different this. + -- Running test case: Set.prototype.pushAll Array: [1,2] => [1,2,"a1","a2"] diff --git a/LayoutTests/inspector/unit-tests/set-utilities.html b/LayoutTests/inspector/unit-tests/set-utilities.html index 830db46aeb43..6caf1ab5b910 100644 --- a/LayoutTests/inspector/unit-tests/set-utilities.html +++ b/LayoutTests/inspector/unit-tests/set-utilities.html @@ -35,6 +35,24 @@ }, }); + suite.addTestCase({ + name: "Set.prototype.some", + test() { + function test(set, callback, thisArg, expected, message) { + InspectorTest.expectEqual(set.some(callback, thisArg), expected, message); + } + + const set1 = new Set([1, 2, 3, 4]); + const set2 = new Set([2, 4]); + + test(set1, function(value) { return value % 2; }, null, true, "Set can 'some' based on the value."); + test(set1, function(value, key) { return key === 5; }, null, false, "Set can 'some' based on the key."); + test(set1, function(value, key, set) { return set === set1; }, null, true, "Set can 'some' based on the set object."); + test(set1, function(value, key, set) { return this.has(key); }, set2, true, "Set can 'some' with a different this."); + test(set1, function(value, key, set) { return this === set; }, set2, false, "Set can 'some' based on the set object with a different this."); + }, + }); + suite.addTestCase({ name: "Set.prototype.pushAll", test() { diff --git a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js index eb545021e768..594a3d533324 100644 --- a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js +++ b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js @@ -197,6 +197,7 @@ localizedStrings["Approximate Number"] = "~%s"; localizedStrings["Area"] = "Area"; /* Label for option to toggle the area names setting for CSS grid overlays */ localizedStrings["Area names @ Layout Panel Overlay Options"] = "Area Names"; +localizedStrings["As such, the contents will be run as though it was typed into the Console."] = "As such, the contents will be run as though it was typed into the Console."; localizedStrings["Assertion"] = "Assertion"; localizedStrings["Assertion Failed"] = "Assertion Failed"; localizedStrings["Assertion Failed: %s"] = "Assertion Failed: %s"; @@ -397,6 +398,10 @@ localizedStrings["Console"] = "Console"; localizedStrings["Console Evaluation"] = "Console Evaluation"; localizedStrings["Console Evaluation %d"] = "Console Evaluation %d"; localizedStrings["Console Profile Recorded"] = "Console Profile Recorded"; +localizedStrings["Console Snippet..."] = "Console Snippet..."; +localizedStrings["Console Snippets"] = "Console Snippets"; +localizedStrings["Console Snippets are an easy way to save and evaluate JavaScript in the Console."] = "Console Snippets are an easy way to save and evaluate JavaScript in the Console."; +localizedStrings["Console Snippets..."] = "Console Snippets..."; /* Name of Console Tab */ localizedStrings["Console Tab Name"] = "Console"; localizedStrings["Console cleared at %s"] = "Console cleared at %s"; @@ -434,6 +439,8 @@ localizedStrings["Create %s Rule"] = "Create %s Rule"; /* Title of button that creates a new audit. */ localizedStrings["Create @ Audit Tab Navigation Sidebar"] = "Create"; localizedStrings["Create Breakpoint"] = "Create Breakpoint"; +localizedStrings["Create Console Snippet"] = "Create Console Snippet"; +localizedStrings["Create Console Snippet..."] = "Create Console Snippet..."; localizedStrings["Create Local Override"] = "Create Local Override"; localizedStrings["Create Request Local Override"] = "Create Request Local Override"; localizedStrings["Create Resource"] = "Create Resource"; @@ -993,8 +1000,10 @@ localizedStrings["Microtask Dispatched"] = "Microtask Dispatched"; localizedStrings["Microtask Fired"] = "Microtask Fired"; localizedStrings["Missing result level"] = "Missing result level"; localizedStrings["Mixed"] = "Mixed"; +localizedStrings["Modifications are saved automatically and will apply the next time the Console Snippet is run."] = "Modifications are saved automatically and will apply the next time the Console Snippet is run."; localizedStrings["Modifications made here will take effect on the next load of any page or sub-frame."] = "Modifications made here will take effect on the next load of any page or sub-frame."; localizedStrings["Module Code"] = "Module Code"; +localizedStrings["More information is available at ."] = "More information is available at ."; localizedStrings["More information is available at ."] = "More information is available at ."; localizedStrings["Multi-Entry"] = "Multi-Entry"; localizedStrings["Name"] = "Name"; @@ -1027,6 +1036,7 @@ localizedStrings["No Canvas Contexts"] = "No Canvas Contexts"; localizedStrings["No Canvas Selected"] = "No Canvas Selected"; localizedStrings["No Chart Available"] = "No Chart Available"; localizedStrings["No Child Layers"] = "No Child Layers"; +localizedStrings["No Console Snippets"] = "No Console Snippets"; localizedStrings["No Data Bindings"] = "No Data Bindings"; localizedStrings["No Enabled Audits"] = "No Enabled Audits"; localizedStrings["No Entries"] = "No Entries"; @@ -1330,6 +1340,7 @@ localizedStrings["Reveal in Style Sheet"] = "Reveal in Style Sheet"; localizedStrings["Role"] = "Role"; /* Property value for `font-variant-alternates: ruby`. */ localizedStrings["Ruby Glyphs @ Font Details Sidebar Property Value"] = "Ruby Glyphs"; +localizedStrings["Run"] = "Run"; localizedStrings["Run %d"] = "Run %d"; localizedStrings["Run console commands as if inside a user gesture"] = "Run console commands as if inside a user gesture"; localizedStrings["Running the \u201C%s\u201D audit"] = "Running the \u201C%s\u201D audit"; @@ -1577,6 +1588,7 @@ localizedStrings["The \u201C%s\u201D audit resulted in a warning"] = "The \u201C localizedStrings["The \u201C%s\u201D audit threw an error"] = "The \u201C%s\u201D audit threw an error"; localizedStrings["The \u201C%s\u201D\ntable is empty."] = "The \u201C%s\u201D\ntable is empty."; localizedStrings["The contents and enabled state will be preserved across Web Inspector sessions."] = "The contents and enabled state will be preserved across Web Inspector sessions."; +localizedStrings["The contents will be preserved across Web Inspector sessions."] = "The contents will be preserved across Web Inspector sessions."; localizedStrings["The page's content has changed"] = "The page's content has changed"; localizedStrings["The resource was requested insecurely."] = "The resource was requested insecurely."; /* Message displayed in a banner when one or more snapshots that the user has not yet seen are being filtered. */ @@ -1619,6 +1631,7 @@ localizedStrings["This is what the result of a passing test with no data looks l localizedStrings["This is what the result of a test that threw an error with no data looks like."] = "This is what the result of a test that threw an error with no data looks like."; localizedStrings["This is what the result of a warning test with no data looks like."] = "This is what the result of a warning test with no data looks like."; localizedStrings["This is what the result of an unsupported test with no data looks like."] = "This is what the result of an unsupported test with no data looks like."; +localizedStrings["This means all of the Console Command Line API is available ."] = "This means all of the Console Command Line API is available ."; localizedStrings["This object is a root"] = "This object is a root"; localizedStrings["This object is referenced by internal objects"] = "This object is referenced by internal objects"; localizedStrings["This resource came from a local override"] = "This resource came from a local override"; diff --git a/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js b/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js index 19620428feb8..a644ca944676 100644 --- a/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js +++ b/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js @@ -67,7 +67,7 @@ WI.ObjectStore = class ObjectStore WI.ObjectStore._databaseCallbacks = [callback]; - const version = 7; // Increment this for every edit to `WI.objectStores`. + const version = 8; // Increment this for every edit to `WI.objectStores`. let databaseRequest = window.indexedDB.open(WI.ObjectStore._databaseName, version); databaseRequest.addEventListener("upgradeneeded", (event) => { @@ -278,4 +278,7 @@ WI.objectStores = { // Version 7 symbolicBreakpoints: new WI.ObjectStore("debugger-symbolic-breakpoints", {keyPath: "__id"}), + + // Version 8 + consoleSnippets: new WI.ObjectStore("console-snippets", {keyPath: "__id"}), }; diff --git a/Source/WebInspectorUI/UserInterface/Base/Utilities.js b/Source/WebInspectorUI/UserInterface/Base/Utilities.js index 71b3442f4e71..186b1af8a307 100644 --- a/Source/WebInspectorUI/UserInterface/Base/Utilities.js +++ b/Source/WebInspectorUI/UserInterface/Base/Utilities.js @@ -208,6 +208,18 @@ Object.defineProperty(Set.prototype, "filter", }, }); +Object.defineProperty(Set.prototype, "some", + { + value(predicate, thisArg) + { + for (let item of this) { + if (predicate.call(thisArg, item, item, this)) + return true; + } + return false; + }, + }); + Object.defineProperty(Set.prototype, "addAll", { value(iterable) diff --git a/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js b/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js index 7ebebc1270f8..27e35d226afe 100644 --- a/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js +++ b/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js @@ -39,9 +39,29 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object this._isNewPageOrReload = false; this._remoteObjectsToRelease = null; + this._customLoggingChannels = []; + + this._snippets = new Set; + this._restoringSnippets = false; + + WI.ConsoleSnippet.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleSnippetContentChanged, this); + WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); - this._customLoggingChannels = []; + WI.Target.registerInitializationPromise((async () => { + let serializedSnippets = await WI.objectStores.consoleSnippets.getAll(); + + this._restoringSnippets = true; + for (let serializedSnippet of serializedSnippets) { + let snippet = WI.ConsoleSnippet.fromJSON(serializedSnippet); + + const key = null; + WI.objectStores.consoleSnippets.associateObject(snippet, key, serializedSnippet); + + this.addSnippet(snippet); + } + this._restoringSnippets = false; + })()); } // Static @@ -66,6 +86,7 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object get warningCount() { return this._warningCount; } get errorCount() { return this._errorCount; } + get snippets() { return this._snippets; } get customLoggingChannels() { return this._customLoggingChannels; } issuesForSourceCode(sourceCode) @@ -88,6 +109,33 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object this._remoteObjectsToRelease.add(remoteObject); } + addSnippet(snippet) + { + console.assert(snippet instanceof WI.ConsoleSnippet, snippet); + console.assert(!this._snippets.has(snippet), snippet); + console.assert(!this._snippets.some((existingSnippet) => snippet.contentIdentifier === existingSnippet.contentIdentifier), snippet); + + this._snippets.add(snippet); + + if (!this._restoringSnippets) + WI.objectStores.consoleSnippets.putObject(snippet); + + this.dispatchEventToListeners(WI.ConsoleManager.Event.SnippetAdded, {snippet}); + } + + removeSnippet(snippet) + { + console.assert(snippet instanceof WI.ConsoleSnippet, snippet); + console.assert(this._snippets.has(snippet), snippet); + + this._snippets.delete(snippet); + + if (!this._restoringSnippets) + WI.objectStores.consoleSnippets.deleteObject(snippet); + + this.dispatchEventToListeners(WI.ConsoleManager.Event.SnippetRemoved, {snippet}); + } + // ConsoleObserver messageWasAdded(target, source, level, text, type, url, line, column, repeatCount, parameters, stackTrace, requestId, timestamp) @@ -216,6 +264,15 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object this.dispatchEventToListeners(WI.ConsoleManager.Event.Cleared); } + _handleSnippetContentChanged(event) + { + let snippet = event.target; + + console.assert(this._snippets.has(snippet), snippet); + + WI.objectStores.consoleSnippets.putObject(snippet); + } + _mainResourceDidChange(event) { console.assert(event.target instanceof WI.Frame); @@ -239,4 +296,6 @@ WI.ConsoleManager.Event = { MessageAdded: "console-manager-message-added", IssueAdded: "console-manager-issue-added", PreviousMessageRepeatCountUpdated: "console-manager-previous-message-repeat-count-updated", + SnippetAdded: "console-manager-snippet-added", + SnippetRemoved: "console-manager-snippet-removed", }; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js b/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js index d860306d10ee..9f99194edd92 100644 --- a/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js +++ b/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js @@ -604,9 +604,7 @@ WI.DOMManager = class DOMManager extends WI.Object // Re-resolve the node in the console's object group when adding to the console. let domNode = this.nodeForId(nodeId); WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup).then((remoteObject) => { - const specialLogStyles = true; - const shouldRevealConsole = false; - WI.consoleLogViewController.appendImmediateExecutionWithResult(WI.UIString("Selected Element"), remoteObject, specialLogStyles, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(WI.UIString("Selected Element"), remoteObject, {addSpecialUserLogClass: true}); }); } diff --git a/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js index ce4242c68830..df8112fa1e83 100644 --- a/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js +++ b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js @@ -45,7 +45,7 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob this._cleared = true; this._previousMessageView = null; - this._lastCommitted = ""; + this._lastCommitted = {text: "", special: false}; this._repeatCountWasInterrupted = false; this._sessions = []; @@ -105,7 +105,7 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob let consoleSession = new WI.ConsoleSession(data); this._previousMessageView = null; - this._lastCommitted = ""; + this._lastCommitted = {text: "", special: false}; this._repeatCountWasInterrupted = false; this._sessions.push(consoleSession); @@ -117,12 +117,19 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob consoleSession.element.scrollIntoView(); } - appendImmediateExecutionWithResult(text, result, addSpecialUserLogClass, shouldRevealConsole) + appendImmediateExecutionWithResult(text, result, {addSpecialUserLogClass, shouldRevealConsole, handleClick} = {}) { console.assert(result instanceof WI.RemoteObject); - var commandMessageView = new WI.ConsoleCommandView(text, addSpecialUserLogClass ? "special-user-log" : null); - this._appendConsoleMessageView(commandMessageView, true); + if (this._lastCommitted.text !== text || this._lastCommitted.special !== addSpecialUserLogClass) { + let classNames = []; + if (addSpecialUserLogClass) + classNames.push("special-user-log"); + + let commandMessageView = new WI.ConsoleCommandView(text, {classNames, handleClick}); + this._appendConsoleMessageView(commandMessageView, true); + this._lastCommitted = {text, special: addSpecialUserLogClass}; + } function saveResultCallback(savedResultIndex) { @@ -218,10 +225,10 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob { console.assert(text); - if (this._lastCommitted !== text) { + if (this._lastCommitted.text !== text || this._lastCommitted.special) { let commandMessageView = new WI.ConsoleCommandView(text); this._appendConsoleMessageView(commandMessageView, true); - this._lastCommitted = text; + this._lastCommitted = {text, special: false}; } function printResult(result, wasThrown, savedResultIndex) @@ -277,7 +284,7 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob this._previousMessageView = messageView; if (messageView.message && messageView.message.source !== WI.ConsoleMessage.MessageSource.JS) - this._lastCommitted = ""; + this._lastCommitted = {test: "", special: false}; if (WI.consoleContentView.isAttached) this.renderPendingMessagesSoon(); diff --git a/Source/WebInspectorUI/UserInterface/Images/Start.svg b/Source/WebInspectorUI/UserInterface/Images/Start.svg new file mode 100644 index 000000000000..52126ecba61d --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Images/Start.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Source/WebInspectorUI/UserInterface/Main.html b/Source/WebInspectorUI/UserInterface/Main.html index 23a29d79ae25..92a6e527b4de 100644 --- a/Source/WebInspectorUI/UserInterface/Main.html +++ b/Source/WebInspectorUI/UserInterface/Main.html @@ -85,6 +85,7 @@ + @@ -423,6 +424,7 @@ + @@ -698,6 +700,7 @@ + diff --git a/Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js b/Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js new file mode 100644 index 000000000000..d26c68340072 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2022 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.ConsoleSnippet = class ConsoleSnippet extends WI.LocalScript +{ + constructor(title, source) + { + console.assert(typeof title === "string" && title.trim().length, title); + + const target = null; + const sourceURL = null; + super(target, title, sourceURL, WI.Script.SourceType.Program, source, {editable: true}); + } + + // Static + + static createDefaultWithTitle(title) + { + const source = ` +/* + * ${WI.UIString("Console Snippets are an easy way to save and evaluate JavaScript in the Console.")} + * + * ${WI.UIString("As such, the contents will be run as though it was typed into the Console.")} + * ${WI.UIString("This means all of the Console Command Line API is available .")} + * + * ${WI.UIString("Modifications are saved automatically and will apply the next time the Console Snippet is run.")} + * ${WI.UIString("The contents will be preserved across Web Inspector sessions.")} + * + * ${WI.UIString("More information is available at .")} + */ + +"Hello World!" +`.trimStart(); + return new WI.ConsoleSnippet(title, source); + } + + // Public + + get title() + { + return this.url; + } + + run() + { + WI.runtimeManager.evaluateInInspectedWindow(this.content, { + objectGroup: WI.RuntimeManager.ConsoleObjectGroup, + includeCommandLineAPI: true, + doNotPauseOnExceptionsAndMuteConsole: true, + generatePreview: true, + saveResult: true, + }, (result, wasThrown) => { + WI.consoleLogViewController.appendImmediateExecutionWithResult(this.title, result, { + addSpecialUserLogClass: true, + shouldRevealConsole: true, + handleClick: (event) => { + if (!WI.consoleManager.snippets.has(this)) + return; + + + const cookie = null; + WI.showRepresentedObject(this, cookie, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }); + }, + }); + }); + } + + // Import / Export + + static fromJSON(json) + { + return new WI.ConsoleSnippet(json.title, json.source); + } + + toJSON(key) + { + let json = { + title: this.title, + source: this.content, + }; + if (key === WI.ObjectStore.toJSONSymbol) + json[WI.objectStores.breakpoints.keyPath] = this.title; + return json; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Test.html b/Source/WebInspectorUI/UserInterface/Test.html index 808d945d4242..f289516904c3 100644 --- a/Source/WebInspectorUI/UserInterface/Test.html +++ b/Source/WebInspectorUI/UserInterface/Test.html @@ -149,6 +149,7 @@ + diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js index 1d135e488f84..4a02006f7414 100644 --- a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js @@ -397,8 +397,7 @@ WI.AnimationContentView = class AnimationContentView extends WI.ContentView return; const text = WI.UIString("Selected Animation", "Appears as a label when a given web animation is logged to the Console"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js b/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js index b54c857d5e55..f83430093ad6 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js +++ b/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js @@ -59,9 +59,6 @@ WI.ButtonNavigationItem = class ButtonNavigationItem extends WI.NavigationItem this.buttonStyle = this._image ? WI.ButtonNavigationItem.Style.Image : WI.ButtonNavigationItem.Style.Text; this.imageType = this._image ? WI.ButtonNavigationItem.ImageType.SVG : null; - - if (this.buttonStyle === WI.ButtonNavigationItem.Style.Image) - this.tooltip = toolTipOrLabel; } // Public @@ -221,16 +218,19 @@ WI.ButtonNavigationItem = class ButtonNavigationItem extends WI.NavigationItem switch (this._imageType) { case null: case WI.ButtonNavigationItem.ImageType.SVG: { - let glyphElement = WI.ImageUtilities.useSVGSymbol(this._image, "glyph"); - glyphElement.style.width = this._imageWidth + "px"; - glyphElement.style.height = this._imageHeight + "px"; - this.element.appendChild(glyphElement); + if (this._image) { + let glyphElement = WI.ImageUtilities.useSVGSymbol(this._image, "glyph"); + glyphElement.style.width = this._imageWidth + "px"; + glyphElement.style.height = this._imageHeight + "px"; + this.element.appendChild(glyphElement); + } break; } case WI.ButtonNavigationItem.ImageType.IMG: { let img = this.element.appendChild(document.createElement("img")); - img.src = this._image; + if (this._image) + img.src = this._image; break; } } @@ -240,6 +240,9 @@ WI.ButtonNavigationItem = class ButtonNavigationItem extends WI.NavigationItem labelElement.textContent = this._label; } } + + if (this._buttonStyle === WI.ButtonNavigationItem.Style.Image) + this.tooltip ||= this._label; } _updateTabIndex() diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js b/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js index 7d870d0872a1..de35abcc0172 100644 --- a/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js @@ -308,8 +308,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView return; const text = WI.UIString("Selected Canvas Context"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js index 91f26a7c301c..613cd1450469 100644 --- a/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js +++ b/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js @@ -93,8 +93,7 @@ WI.CanvasTreeElement = class CanvasTreeElement extends WI.FolderizedTreeElement return; const text = WI.UIString("Selected Canvas Context"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js b/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js index 8379ed7f419e..300760462b41 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js @@ -29,10 +29,11 @@ WI.ConsoleCommandView = class ConsoleCommandView { - constructor(commandText, className) + constructor(commandText, {classNames, handleClick} = {}) { this._commandText = commandText; - this._className = className || ""; + this._classNames = classNames || null; + this._handleClick = handleClick || null; } // Public @@ -44,12 +45,14 @@ WI.ConsoleCommandView = class ConsoleCommandView this._element.dir = "ltr"; this._element.setAttribute("data-labelprefix", WI.UIString("Input: ")); - if (this._className) - this._element.classList.add(this._className); + if (this._classNames) + this._element.classList.add(...this._classNames); this._formattedCommandElement = this._element.appendChild(document.createElement("span")); this._formattedCommandElement.classList.add("console-message-body"); this._formattedCommandElement.textContent = this._commandText; + if (this._handleClick) + this._formattedCommandElement.addEventListener("click", this._handleClick); // FIXME: Web Inspector: LogContentView should use higher level objects this._element.__commandView = this; diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css b/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css index 80583d732b77..1f9713faa125 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css @@ -44,7 +44,7 @@ pointer-events: none; } -.console-drawer > .navigation-bar > .item > .glyph, +.console-drawer > .navigation-bar > .item > :is(.glyph, img), .console-drawer > .navigation-bar > .log-scope-bar > li { pointer-events: all; } diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css new file mode 100644 index 000000000000..a83058c0ff7c --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 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. + */ + + .tree-outline .item.console-snippet > .status { + display: flex; + align-items: center; + width: 16px; + height: 16px; + } + + .tree-outline .item.console-snippet > .status > img { + width: 100%; + height: 100%; + content: url(../Images/Start.svg); + } + +.tree-outline .item.console-snippet:not(:hover) > .status > img { + display: none; +} + +body:not(.window-inactive, .window-docked-inactive) .tree-outline:focus-within .item.console-snippet.selected:hover > .status > img { + filter: var(--filter-invert); +} + +@media (prefers-color-scheme: dark) { + .tree-outline .item.console-snippet:hover > .status > img { + filter: var(--filter-invert) brightness(90%); + } +} diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js new file mode 100644 index 000000000000..4c8de43e3078 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 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.ConsoleSnippetTreeElement = class ConsoleSnippetTreeElement extends WI.ScriptTreeElement +{ + constructor(consoleSnippet) + { + console.assert(consoleSnippet instanceof WI.ConsoleSnippet, consoleSnippet); + + super(consoleSnippet); + + this.addClassName("console-snippet"); + } + + // Protected + + onattach() + { + super.onattach(); + + this.status = document.createElement("img"); + this.status.title = WI.UIString("Run"); + this.status.addEventListener("click", (event) => { + this.representedObject.run(); + }); + } + + ondelete() + { + WI.consoleManager.removeSnippet(this.representedObject); + + return true; + } + + onspace() + { + this.representedObject.run(); + + return true; + } + + canSelectOnMouseDown(event) + { + if (this.status.contains(event.target)) + return false; + + return super.canSelectOnMouseDown(event); + } + + updateStatus() + { + // Do nothing. Do not allow ScriptTreeElement / SourceCodeTreeElement to modify our status element. + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js b/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js index c9a486e97ede..0553e52c94cd 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js +++ b/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js @@ -363,8 +363,7 @@ WI.appendContextMenuItemsForDOMNode = function(contextMenu, domNode, options = { contextMenu.appendItem(label, () => { WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup).then((remoteObject) => { let text = isElement ? WI.UIString("Selected Element", "Selected DOM element") : WI.UIString("Selected Node", "Selected DOM node"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); } diff --git a/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js b/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js index 10185b8a0ad0..54a9d4fa666b 100644 --- a/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js +++ b/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js @@ -81,12 +81,12 @@ WI.HeapSnapshotInstanceDataGridNode = class HeapSnapshotInstanceDataGridNode ext if (node.className === "string") { WI.heapManager.getPreview(node, function(error, string, functionDetails, objectPreviewPayload) { let remoteObject = error ? WI.RemoteObject.fromPrimitiveValue(undefined) : WI.RemoteObject.fromPrimitiveValue(string); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass, shouldRevealConsole}); }); } else { WI.heapManager.getRemoteObject(node, WI.RuntimeManager.ConsoleObjectGroup, function(error, remoteObjectPayload) { let remoteObject = error ? WI.RemoteObject.fromPrimitiveValue(undefined) : WI.RemoteObject.fromPayload(remoteObjectPayload, WI.assumingMainTarget()); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass, shouldRevealConsole}); }); } }); diff --git a/Source/WebInspectorUI/UserInterface/Views/InputPopover.js b/Source/WebInspectorUI/UserInterface/Views/InputPopover.js index 01a31ee52d55..d8eb4ed5a96e 100644 --- a/Source/WebInspectorUI/UserInterface/Views/InputPopover.js +++ b/Source/WebInspectorUI/UserInterface/Views/InputPopover.js @@ -25,13 +25,12 @@ WI.InputPopover = class InputPopover extends WI.Popover { - constructor(message, delegate) + constructor(identifier, message, delegate) { super(delegate); + this._identifier = identifier; this._message = message; - this._value = null; - this._result = WI.InputPopover.Result.None; this._targetElement = null; this._preferredEdges = null; @@ -39,8 +38,11 @@ WI.InputPopover = class InputPopover extends WI.Popover this.windowResizeHandler = this._presentOverTargetElement.bind(this); } - get value() { return this._value; } - get result() { return this._result; } + get identifier() { return this._identifier; } + get value() + { + return this._inputElement?.value ?? ""; + } show(targetElement, preferredEdges) { @@ -48,7 +50,7 @@ WI.InputPopover = class InputPopover extends WI.Popover this._preferredEdges = preferredEdges; let contentElement = document.createElement("div"); - contentElement.classList.add("input-popover-content"); + contentElement.classList.add("input-popover-content", this._identifier); if (this._message) { let label = document.createElement("div"); @@ -61,13 +63,8 @@ WI.InputPopover = class InputPopover extends WI.Popover this._inputElement.spellcheck = false; this._inputElement.addEventListener("keydown", (event) => { - if (!isEnterKey(event)) - return; - - this._result = WI.InputPopover.Result.Committed; - this._value = event.target.value; - - this.dismiss(); + if (isEnterKey(event)) + this.dismiss(); }); contentElement.appendChild(this._inputElement); @@ -89,9 +86,3 @@ WI.InputPopover = class InputPopover extends WI.Popover this._inputElement.select(); } }; - -WI.InputPopover.Result = { - None: Symbol("result-none"), - Cancelled: Symbol("result-cancelled"), - Committed: Symbol("result-committed"), -}; diff --git a/Source/WebInspectorUI/UserInterface/Views/LogContentView.css b/Source/WebInspectorUI/UserInterface/Views/LogContentView.css index 31604e705ae8..44f6622f1d84 100644 --- a/Source/WebInspectorUI/UserInterface/Views/LogContentView.css +++ b/Source/WebInspectorUI/UserInterface/Views/LogContentView.css @@ -61,6 +61,12 @@ color: var(--glyph-color-active-pressed); } +.navigation-bar > .item.console-snippets > img { + width: 16px; + height: 16px; + content: url(../Images/ClippingIcons.svg#js-light); +} + .content-view.log { display: flex; flex-direction: column; @@ -267,7 +273,24 @@ body[dir=rtl] .console-group-title::before { alt: attr(data-labelprefix); } +.console-user-command:not(:hover) > .console-snippet-options { + display: none; +} + +.create-snippet-popover > .label { + display: inline-block; + margin-inline-end: 6px; + font-weight: bold; + line-height: 23px; + white-space: nowrap; + color: hsl(0, 0%, 34%); +} + @media (prefers-color-scheme: dark) { + .navigation-bar > .item.console-snippets > img { + content: url(../Images/ClippingIcons.svg#js-dark); + } + .console-messages { background-color: var(--background-color-content); } diff --git a/Source/WebInspectorUI/UserInterface/Views/LogContentView.js b/Source/WebInspectorUI/UserInterface/Views/LogContentView.js index 7bb2d5c7dc86..99cea9c5f83d 100644 --- a/Source/WebInspectorUI/UserInterface/Views/LogContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/LogContentView.js @@ -116,6 +116,12 @@ WI.LogContentView = class LogContentView extends WI.ContentView this._messageSourceBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._messageSourceBarSelectionDidChange, this); } + const consoleSnippetsImage = ""; // This is set in CSS to have dark mode support. + this._consoleSnippetsNavigationItem = new WI.ButtonNavigationItem("console-snippets", WI.UIString("Console Snippets..."), consoleSnippetsImage); + this._consoleSnippetsNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.Image; + this._consoleSnippetsNavigationItem.imageType = WI.ButtonNavigationItem.ImageType.IMG; + WI.addMouseDownContextMenuHandlers(this._consoleSnippetsNavigationItem.element, this._handleSnippetsNavigationItemContextMenu.bind(this)); + this._garbageCollectNavigationItem = new WI.ButtonNavigationItem("garbage-collect", WI.UIString("Collect garbage"), "Images/NavigationItemGarbageCollect.svg", 16, 16); this._garbageCollectNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._garbageCollect, this); @@ -156,6 +162,8 @@ WI.LogContentView = class LogContentView extends WI.ContentView if (this._emulateUserGestureNavigationItemGroup) navigationItems.push(this._emulateUserGestureNavigationItemGroup); + navigationItems.push(this._consoleSnippetsNavigationItem); + if (InspectorBackend.hasCommand("Heap.gc")) navigationItems.push(this._garbageCollectNavigationItem); @@ -377,6 +385,37 @@ WI.LogContentView = class LogContentView extends WI.ContentView this.prompt.focus(); } + // Popover delegate + + willDismissPopover(popover) + { + if (popover instanceof WI.InputPopover) { + let title = popover.value?.trim(); + if (!title) { + InspectorFrontendHost.beep(); + return; + } + + // Do not conflict with an existing console snippet. + if (WI.consoleManager.snippets.some((consoleSnippet) => consoleSnippet.title === title)) { + InspectorFrontendHost.beep(); + return; + } + + let consoleSnippet = WI.ConsoleSnippet.createDefaultWithTitle(title); + WI.consoleManager.addSnippet(consoleSnippet); + + const cookie = null; + WI.showRepresentedObject(consoleSnippet, cookie, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }); + return; + } + + console.assert(false, "not reached", popover); + } + // Private _formatMessagesAsData(onlySelected) @@ -668,8 +707,7 @@ WI.LogContentView = class LogContentView extends WI.ContentView WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup) .then((remoteObject) => { let text = domNode.nodeType() === Node.ELEMENT_NODE ? WI.UIString("Dropped Element") : WI.UIString("Dropped Node"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); this.prompt.focus(); }); @@ -926,6 +964,21 @@ WI.LogContentView = class LogContentView extends WI.ContentView }, !WI.settings.clearLogOnNavigate.value); } + _handleSnippetsNavigationItemContextMenu(contextMenu) { + for (let consoleSnippet of WI.consoleManager.snippets) { + contextMenu.appendItem(consoleSnippet.displayName, () => { + consoleSnippet.run(); + }); + } + + contextMenu.appendSeparator(); + + contextMenu.appendItem(WI.UIString("Create Console Snippet..."), () => { + let popover = new WI.InputPopover("create-snippet-popover", WI.UIString("Name"), this); + popover.show(this._consoleSnippetsNavigationItem.element, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); + }); + } + _handleClearLogOnNavigateSettingChanged() { this._updateOtherFiltersNavigationItemState(); diff --git a/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js b/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js index 48ff08c63856..042a23c2f47c 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js +++ b/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js @@ -295,7 +295,7 @@ WI.ObjectPreviewView = class ObjectPreviewView extends WI.Object if (!isImpossible) WI.quickConsole.prompt.pushHistoryItem(text); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, this._remoteObject, isImpossible); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, this._remoteObject, {addSpecialUserLogClass: isImpossible}); }); } }; diff --git a/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js index c36d44f62380..b00f12044ff4 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js +++ b/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js @@ -196,7 +196,7 @@ WI.ObjectTreeBaseTreeElement = class ObjectTreeBaseTreeElement extends WI.Genera return; var text = WI.UIString("Selected Symbol"); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, symbol, true); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, symbol, {addSpecialUserLogClass: true}); } _logValue(value) @@ -212,7 +212,7 @@ WI.ObjectTreeBaseTreeElement = class ObjectTreeBaseTreeElement extends WI.Genera if (!isImpossible) WI.quickConsole.prompt.pushHistoryItem(text); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, resolvedValue, isImpossible); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, resolvedValue, {addSpecialUserLogClass: isImpossible}); } _appendMenusItemsForObject(contextMenu, resolvedValue) diff --git a/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js b/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js index c24067b467bc..52867a2d82f5 100644 --- a/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js +++ b/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js @@ -165,6 +165,8 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.ScriptRemoved, this._scriptRemoved, this); WI.cssManager.removeEventListener(WI.CSSManager.Event.StyleSheetAdded, this._handleStyleSheetAdded, this); WI.cssManager.removeEventListener(WI.CSSManager.Event.StyleSheetRemoved, this._handleStyleSheetRemoved, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); this._queryController.reset(); this._updateFilterThrottler.cancel(); @@ -180,6 +182,8 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ScriptRemoved, this._scriptRemoved, this); WI.cssManager.addEventListener(WI.CSSManager.Event.StyleSheetAdded, this._handleStyleSheetAdded, this); WI.cssManager.addEventListener(WI.CSSManager.Event.StyleSheetRemoved, this._handleStyleSheetRemoved, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); if (WI.networkManager.mainFrame) this._addResourcesForFrame(WI.networkManager.mainFrame); @@ -206,6 +210,9 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog this._addResource(styleSheet); } + for (let consoleSnippet of WI.consoleManager.snippets) + this._addResource(consoleSnippet); + this._updateFilterThrottler.force(); this._inputElement.focus(); @@ -469,6 +476,16 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog this._removeResource(styleSheet); } + + _handleConsoleSnippetAdded(event) + { + this._addResource(event.data.snippet); + } + + _handleConsoleSnippetRemoved(event) + { + this._removeResource(event.data.snippet); + } }; WI.OpenResourceDialog.ResourceMatchCookieDataSymbol = Symbol("open-resource-dialog-resource-match-cookie-data"); diff --git a/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js b/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js index 5c51a3a4c0be..420d6e6bfe6f 100644 --- a/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js +++ b/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js @@ -336,8 +336,7 @@ WI.QuickConsole = class QuickConsole extends WI.View WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup) .then((remoteObject) => { let text = domNode.nodeType() === Node.ELEMENT_NODE ? WI.UIString("Dropped Element") : WI.UIString("Dropped Node"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); this.prompt.focus(); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css b/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css index d5c3e2dc94ba..33453e54af4f 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css +++ b/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css @@ -71,7 +71,7 @@ content: url(../Images/DocumentIcons.svg#js-light-override); } -.anonymous-script-icon .icon { +:is(.anonymous-script-icon, .resource-icon.resource-type-script.console-snippet) .icon { content: url(../Images/ClippingIcons.svg#js-light); } @@ -162,7 +162,7 @@ body:not(.window-inactive, .window-docked-inactive) .tree-outline:focus-within . content: url(../Images/DocumentIcons.svg#js-dark-override); } - .anonymous-script-icon .icon { + :is(.anonymous-script-icon, .resource-icon.resource-type-script.console-snippet) .icon { content: url(../Images/ClippingIcons.svg#js-dark); } diff --git a/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js b/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js index a8bd0f178a8d..64e10d0cb84a 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js @@ -248,7 +248,10 @@ WI.ScriptContentView = class ScriptContentView extends WI.ContentView _handleTextEditorContentDidChange(event) { - this._script.editableRevision.updateRevisionContent(this._textEditor.string); + this._updateRevisionContentDebouncer ||= new Debouncer(() => { + this._script.editableRevision.updateRevisionContent(this._textEditor.string); + }); + this._updateRevisionContentDebouncer.delayForTime(250); } _togglePrettyPrint(event) diff --git a/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js index a065be94f06c..aa1ef3016022 100644 --- a/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js +++ b/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js @@ -343,6 +343,8 @@ WI.SearchSidebarPanel = class SearchSidebarPanel extends WI.NavigationSidebarPan // FIXME: Resource search should work with Local Overrides if enabled. + // FIXME: Resource search should work with Console Snippets. + // FIXME: Resource search should work in JSContext inspection. // Web Inspector: JSContext inspection Resource search does not work } diff --git a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css index 1a0c3036f51d..ecdfc510b049 100644 --- a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css +++ b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css @@ -66,14 +66,10 @@ display: none; } -.sidebar > .panel.navigation.sources > .content > :matches(.pause-reason-container, .call-stack-container, .breakpoints-container, .local-overrides-container) { +.sidebar > .panel.navigation.sources > .content > :matches(.pause-reason-container, .call-stack-container, .breakpoints-container, .local-overrides-container, .console-snippets-container) { border-bottom: 1px solid var(--border-color); } -.sidebar > .panel.navigation.sources > .content > .local-overrides { - border-width: 1px !important; -} - .sidebar > .panel.navigation.sources > .content .details-section { font-size: 11px; --details-section-border-bottom: none; @@ -130,7 +126,7 @@ --details-section-header-top: 0; } - .sidebar > .panel.navigation.sources > .content > :matches(.call-stack-container, .breakpoints-container, .resources-container, .local-overrides-container) { + .sidebar > .panel.navigation.sources > .content > :matches(.call-stack-container, .breakpoints-container, .resources-container, .local-overrides-container, .console-snippets-container) { height: 100%; overflow-y: auto; } @@ -153,6 +149,22 @@ max-height: fit-content; } + .sidebar > .panel.navigation.sources > .content > .console-snippets-container { + flex-grow: 0; + flex-shrink: 0; + height: auto; + } + + .sidebar > .panel.navigation.sources > .content > .console-snippets-container > .details-section { + height: 100%; + } + + .sidebar > .panel.navigation.sources > .content > .console-snippets-container > .details-section > .content { + height: 100%; + max-height: 90px; + overflow-y: auto; + } + .sidebar > .panel.navigation.sources > .content > .resources-container { flex-grow: 1; flex-shrink: 3; diff --git a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js index a864eb314e32..607f99a03dde 100644 --- a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js +++ b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js @@ -246,6 +246,28 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W this._localOverridesContainer.hidden = true; this._localOverridesContainer.appendChild(this._localOverridesSection.element); + this._consoleSnippetsTreeOutline = this.createContentTreeOutline({suppressFiltering: true}); + this._consoleSnippetsTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._handleTreeSelectionDidChange, this); + + this._consoleSnippetsRow = new WI.DetailsSectionRow(WI.UIString("No Console Snippets")); + + let consoleSnippetNavigationBarWrapper = document.createElement("div"); + + let consoleSnippetNavigationBar = new WI.NavigationBar; + consoleSnippetNavigationBarWrapper.appendChild(consoleSnippetNavigationBar.element); + + this._createConsoleSnippetButton = new WI.ButtonNavigationItem("create-console-snippet", WI.UIString("Create Console Snippet"), "Images/Plus13.svg", 13, 13); + this._createConsoleSnippetButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCreateConsoleSnippetButtonClicked, this); + consoleSnippetNavigationBar.addNavigationItem(this._createConsoleSnippetButton); + + let consoleSnippetsGroup = new WI.DetailsSectionGroup([this._consoleSnippetsRow]); + this._consoleSnippetsSection = new WI.DetailsSection("console-snippets", WI.UIString("Console Snippets"), [consoleSnippetsGroup], consoleSnippetNavigationBarWrapper); + + this._consoleSnippetsContainer = this.contentView.element.appendChild(document.createElement("div")); + this._consoleSnippetsContainer.classList.add("console-snippets-container"); + this._consoleSnippetsContainer.hidden = true; + this._consoleSnippetsContainer.appendChild(this._consoleSnippetsSection.element); + this._resourcesNavigationBar = new WI.NavigationBar; this.contentView.addSubview(this._resourcesNavigationBar); @@ -344,6 +366,8 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W WI.consoleManager.addEventListener(WI.ConsoleManager.Event.IssueAdded, this._handleConsoleIssueAdded, this); WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, this._handleConsoleCleared, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this); @@ -438,6 +462,9 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W this._handleAuditManagerTestScheduled(); } + for (let consoleSnippet of WI.consoleManager.snippets) + this._addConsoleSnippet(consoleSnippet); + this._updateBreakpointsDisabledBanner(); } @@ -507,6 +534,8 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.IssueAdded, this._handleConsoleIssueAdded, this); WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.Cleared, this._handleConsoleCleared, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this); @@ -552,6 +581,9 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W if (representedObject instanceof WI.Script && representedObject === WI.networkManager.bootstrapScript) return this._localOverridesTreeOutline.findTreeElement(representedObject); + if (representedObject instanceof WI.ConsoleSnippet) + return this._consoleSnippetsTreeOutline.findTreeElement(representedObject); + if (!this._mainFrameTreeElement && (representedObject instanceof WI.Resource || representedObject instanceof WI.Frame || representedObject instanceof WI.Collection)) { // All resources are under the main frame, so we need to return early if we don't have the main frame yet. return null; @@ -761,7 +793,13 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W treeOutline.addEventListener(WI.TreeOutline.Event.ElementRevealed, function(event) { let treeElement = event.data.element; - let detailsSections = [this._pauseReasonSection, this._callStackSection, this._breakpointsSection, this._localOverridesSection]; + let detailsSections = [ + this._pauseReasonSection, + this._callStackSection, + this._breakpointsSection, + this._localOverridesSection, + this._consoleSnippetsSection, + ]; let detailsSection = detailsSections.find((detailsSection) => detailsSection.element.contains(treeElement.listItemElement)); if (!detailsSection) return; @@ -845,6 +883,11 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W return; } + if (popover instanceof WI.InputPopover) { + this._willDismissConsoleSnippetPopover(popover); + return; + } + if (popover instanceof WI.EventBreakpointPopover) { this._willDismissEventBreakpointPopover(popover); return; @@ -887,6 +930,33 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W WI.showLocalResourceOverride(localResourceOverride); } + _willDismissConsoleSnippetPopover(popover) + { + let title = popover.value?.trim(); + if (!title) { + if (!this._consoleSnippetsTreeOutline.children.length) + this._consoleSnippetsContainer.hidden = true; + + InspectorFrontendHost.beep(); + return; + } + + // Do not conflict with an existing consoleSnippet. + if (WI.consoleManager.snippets.some((consoleSnippet) => consoleSnippet.title === title)) { + InspectorFrontendHost.beep(); + return; + } + + let consoleSnippet = WI.ConsoleSnippet.createDefaultWithTitle(title); + WI.consoleManager.addSnippet(consoleSnippet); + + const cookie = null; + WI.showRepresentedObject(consoleSnippet, cookie, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }); + } + _willDismissEventBreakpointPopover(popover) { let breakpoint = popover.breakpoint; @@ -945,6 +1015,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W const rankFunctions = [ (treeElement) => treeElement instanceof WI.ScriptTreeElement && treeElement.representedObject === WI.networkManager.bootstrapScript, (treeElement) => treeElement instanceof WI.LocalResourceOverrideTreeElement, + (treeElement) => treeElement instanceof WI.ScriptTreeElement && treeElement.representedObject instanceof WI.ConsoleSnippet, (treeElement) => treeElement instanceof WI.CSSStyleSheetTreeElement && treeElement.representedObject.isInspectorStyleSheet(), (treeElement) => treeElement === this._mainFrameTreeElement, (treeElement) => treeElement instanceof WI.FrameTreeElement, @@ -1547,6 +1618,46 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W } } + _addConsoleSnippet(consoleSnippet) + { + console.assert(consoleSnippet instanceof WI.ConsoleSnippet, consoleSnippet); + + if (this._consoleSnippetsTreeOutline.findTreeElement(consoleSnippet)) + return; + + let parentTreeElement = this._consoleSnippetsTreeOutline; + + let consoleSnippetTreeElement = new WI.ConsoleSnippetTreeElement(consoleSnippet); + + let index = insertionIndexForObjectInListSortedByFunction(consoleSnippetTreeElement, parentTreeElement.children, this._boundCompareTreeElements); + parentTreeElement.insertChild(consoleSnippetTreeElement, index); + + this._consoleSnippetsRow.hideEmptyMessage(); + this._consoleSnippetsRow.element.appendChild(this._consoleSnippetsTreeOutline.element); + this._consoleSnippetsContainer.hidden = false; + } + + _removeConsoleSnippet(consoleSnippet) + { + console.assert(consoleSnippet instanceof WI.ConsoleSnippet, consoleSnippet); + + let consoleSnippetTreeElement = this._consoleSnippetsTreeOutline.findTreeElement(consoleSnippet); + if (!consoleSnippetTreeElement) + return; + + let wasSelected = consoleSnippetTreeElement.selected; + + let parentTreeElement = this._consoleSnippetsTreeOutline; + parentTreeElement.removeChild(consoleSnippetTreeElement); + + if (!parentTreeElement.children.length) { + this._consoleSnippetsContainer.hidden = true; + + if (wasSelected && WI.networkManager.mainFrame && WI.networkManager.mainFrame.mainResource) + WI.showRepresentedObject(WI.networkManager.mainFrame.mainResource); + } + } + _updateTemporarilyDisabledBreakpointsButtons() { let breakpointsDisabledTemporarily = WI.debuggerManager.breakpointsDisabledTemporarily; @@ -2159,6 +2270,12 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W } } + _handleCreateConsoleSnippetButtonClicked(event) + { + let popover = new WI.InputPopover("create-snippet-popover", WI.UIString("Name"), this); + popover.show(this._createConsoleSnippetButton.element, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); + } + _populateCreateResourceContextMenu(contextMenu) { if (WI.NetworkManager.supportsOverridingResponses()) { @@ -2176,6 +2293,19 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W }); } + contextMenu.appendItem(WI.UIString("Console Snippet..."), () => { + if (!this._consoleSnippetsTreeOutline.children.length) + this._consoleSnippetsRow.showEmptyMessage(); + + this._consoleSnippetsContainer.hidden = false; + + this._createConsoleSnippetButton.element.scrollIntoViewIfNeeded(false); + requestAnimationFrame(() => { + let popover = new WI.InputPopover("create-snippet-popover", WI.UIString("Name"), this); + popover.show(this._createConsoleSnippetButton.element, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); + }); + }); + if (WI.NetworkManager.supportsBootstrapScript()) { contextMenu.appendItem(WI.UIString("Inspector Bootstrap Script"), async () => { await WI.networkManager.createBootstrapScript(); @@ -2546,6 +2676,16 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W issueTreeElements.forEach((treeElement) => treeElement.parent.removeChild(treeElement)); } + _handleConsoleSnippetAdded(event) + { + this._addConsoleSnippet(event.data.snippet); + } + + _handleConsoleSnippetRemoved(event) + { + this._removeConsoleSnippet(event.data.snippet); + } + _handleTimelineCapturingStateChanged(event) { this._updateTemporarilyDisabledBreakpointsButtons(); diff --git a/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js b/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js index 2f904c6d1892..3e6c6b192ad1 100644 --- a/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js +++ b/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js @@ -53,9 +53,7 @@ WI.WebSocketDataGridNode = class WebSocketDataGridNode extends WI.DataGridNode console.assert(!wasThrown); const title = WI.UIString("Selected Frame"); - const addSpecialUserLogClass = true; - const shouldRevealConsole = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(title, result, addSpecialUserLogClass, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(title, result, {addSpecialUserLogClass: true, shouldRevealConsole: true}); }; if (this._data.isText) { diff --git a/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js index c13d5090a81e..8ff00a92cc1e 100644 --- a/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js +++ b/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js @@ -51,8 +51,7 @@ WI.WebSocketResourceTreeElement = class WebSocketResourceTreeElement extends WI. return; const text = WI.UIString("Selected WebSocket"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); });