From 702f0ad086dbc32b3dd71af757b3ca98b44b111b Mon Sep 17 00:00:00 2001 From: Raphael Ferrand Date: Thu, 18 Nov 2021 12:52:22 +0000 Subject: [PATCH] Bug 1498224 - [devtools] Apply new classnames as you type in the .cls section of the rule-view r=jdescottes Differential Revision: https://phabricator.services.mozilla.com/D129515 --- devtools/client/framework/selection.js | 2 + .../inspector/rules/models/class-list.js | 36 ++++++++++++---- .../test/browser_rules_class_panel_add.js | 2 + .../browser_rules_class_panel_autocomplete.js | 37 +++++++++++++++-- .../rules/views/class-list-previewer.js | 41 ++++++++++++++----- 5 files changed, 96 insertions(+), 22 deletions(-) diff --git a/devtools/client/framework/selection.js b/devtools/client/framework/selection.js index a3102f1964dd..844a646a5f11 100644 --- a/devtools/client/framework/selection.js +++ b/devtools/client/framework/selection.js @@ -192,6 +192,8 @@ Selection.prototype = { return; } + this.emit("node-front-will-unset"); + this._isSlotted = isSlotted; this._nodeFront = nodeFront; diff --git a/devtools/client/inspector/rules/models/class-list.js b/devtools/client/inspector/rules/models/class-list.js index a8cd4b03eae4..122db35685fb 100644 --- a/devtools/client/inspector/rules/models/class-list.js +++ b/devtools/client/inspector/rules/models/class-list.js @@ -38,6 +38,7 @@ class ClassList { this.inspector.on("markupmutation", this.onMutations); this.classListProxyNode = this.inspector.panelDoc.createElement("div"); + this.previewClasses = ""; } destroy() { @@ -72,11 +73,13 @@ class ClassList { if (!CLASSES.has(this.currentNode)) { // Use the proxy node to get a clean list of classes. this.classListProxyNode.className = this.currentNode.className; - const nodeClasses = [ - ...new Set([...this.classListProxyNode.classList]), - ].map(name => { - return { name, isApplied: true }; - }); + const nodeClasses = [...new Set([...this.classListProxyNode.classList])] + .filter( + className => !this.previewClasses.split(" ").includes(className) + ) + .map(name => { + return { name, isApplied: true }; + }); CLASSES.set(this.currentNode, nodeClasses); } @@ -89,10 +92,15 @@ class ClassList { * enabled classes are added. */ get currentClassesPreview() { - return this.currentClasses + const currentClasses = this.currentClasses .filter(({ isApplied }) => isApplied) - .map(({ name }) => name) - .join(" "); + .map(({ name }) => name); + const previewClasses = this.previewClasses + .split(" ") + .filter(previewClass => !currentClasses.includes(previewClass)) + .filter(item => item !== ""); + + return currentClasses.concat(previewClasses).join(" "); } /** @@ -121,6 +129,7 @@ class ClassList { */ addClassName(classNameString) { this.classListProxyNode.className = classNameString; + this.eraseClassPreview(); return Promise.all( [...new Set([...this.classListProxyNode.classList])].map(name => { return this.addClass(name); @@ -211,6 +220,17 @@ class ClassList { this.currentNode ); } + + previewClass(inputClasses) { + if (this.previewClasses !== inputClasses) { + this.previewClasses = inputClasses; + this.applyClassState(); + } + } + + eraseClassPreview() { + this.previewClass(""); + } } module.exports = ClassList; diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_add.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_add.js index 3eaddcfac7c4..598b0c7057f9 100644 --- a/devtools/client/inspector/rules/test/browser_rules_class_panel_add.js +++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_add.js @@ -70,7 +70,9 @@ add_task(async function() { info(`Enter the test string in the field: ${textEntered}`); for (const key of textEntered.split("")) { + const onPreviewMutation = inspector.once("markupmutation"); EventUtils.synthesizeKey(key, {}, view.styleWindow); + await onPreviewMutation; } info("Submit the change and wait for the textfield to become empty"); diff --git a/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js b/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js index e7239d2025fe..f5e790b82be0 100644 --- a/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js +++ b/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js @@ -45,6 +45,7 @@ add_task(async function() { const { autocompletePopup } = view.classListPreviewer; let onPopupOpened = autocompletePopup.once("popup-opened"); EventUtils.synthesizeKey("a", {}, view.styleWindow); + await waitForClassApplied("a"); await onPopupOpened; await checkAutocompleteItems( autocompletePopup, @@ -55,17 +56,14 @@ add_task(async function() { info( "Test that typing more letters filters the autocomplete popup and uses the cache mechanism" ); - const onCacheHit = inspector.inspectorFront.pageStyle.once( - "getAttributesInOwnerDocument-cache-hit" - ); EventUtils.sendString("uto-b", view.styleWindow); + await waitForClassApplied("auto-b"); await checkAutocompleteItems( autocompletePopup, allClasses.filter(cls => cls.startsWith("auto-b")), "The autocomplete popup was filtered with the content of the input" ); - await onCacheHit; ok(true, "The results were retrieved from the cache mechanism"); info("Test that autocomplete shows up-to-date results"); @@ -76,10 +74,12 @@ add_task(async function() { content.document.body.classList.add("auto-body-added-by-script"); }); await onNewMutation; + await waitForClassApplied("auto-body-added-by-script"); // input is now auto-body onPopupOpened = autocompletePopup.once("popup-opened"); EventUtils.sendString("ody", view.styleWindow); + await waitForClassApplied("auto-body"); await onPopupOpened; await checkAutocompleteItems( autocompletePopup, @@ -96,6 +96,7 @@ add_task(async function() { // input is now auto-bodyy let onPopupClosed = autocompletePopup.once("popup-closed"); EventUtils.synthesizeKey("y", {}, view.styleWindow); + await waitForClassApplied("auto-bodyy"); await onPopupClosed; ok(true, "The popup was closed as expected"); await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared"); @@ -108,6 +109,7 @@ add_task(async function() { onPopupOpened = autocompletePopup.once("popup-opened"); EventUtils.synthesizeKey("a", {}, view.styleWindow); + await waitForClassApplied("a"); await onPopupOpened; await checkAutocompleteItems( @@ -147,6 +149,7 @@ add_task(async function() { onPopupClosed = autocompletePopup.once("popup-closed"); EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow); + await waitForClassApplied("auto-body-added-by-script"); await onPopupClosed; is( textInput.value, @@ -157,6 +160,7 @@ add_task(async function() { // Backspace to show the list again onPopupOpened = autocompletePopup.once("popup-opened"); EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow); + await waitForClassApplied("auto-body-added-by-scrip"); await onPopupOpened; is( textInput.value, @@ -172,6 +176,7 @@ add_task(async function() { // Enter to accept onPopupClosed = autocompletePopup.once("popup-closed"); EventUtils.synthesizeKey("KEY_Enter", {}, view.styleWindow); + await waitForClassRemoved("auto-body-added-by-scrip"); await onPopupClosed; is( textInput.value, @@ -182,6 +187,7 @@ add_task(async function() { // Backspace to show again onPopupOpened = autocompletePopup.once("popup-opened"); EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow); + await waitForClassApplied("auto-body-added-by-scrip"); await onPopupOpened; is( textInput.value, @@ -203,6 +209,7 @@ add_task(async function() { "auto-body-added-by-script", "Tab puts the selected item in the input and closes the popup" ); + await waitForClassRemoved("auto-body-added-by-scrip"); }); async function checkAutocompleteItems( @@ -224,3 +231,25 @@ function getAutocompleteItems(autocompletePopup) { el => el.textContent ); } + +async function waitForClassApplied(cls) { + info("Wait for class to be applied: " + cls); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [cls], async _cls => { + return ContentTaskUtils.waitForCondition(() => + content.document.body.classList.contains(_cls) + ); + }); + // Wait for debounced functions to be executed + await wait(200); +} + +async function waitForClassRemoved(cls) { + info("Wait for class to be removed: " + cls); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [cls], async _cls => { + return ContentTaskUtils.waitForCondition( + () => !content.document.body.classList.contains(_cls) + ); + }); + // Wait for debounced functions to be executed + await wait(200); +} diff --git a/devtools/client/inspector/rules/views/class-list-previewer.js b/devtools/client/inspector/rules/views/class-list-previewer.js index c30103c48a49..21105567bcb6 100644 --- a/devtools/client/inspector/rules/views/class-list-previewer.js +++ b/devtools/client/inspector/rules/views/class-list-previewer.js @@ -31,13 +31,14 @@ class ClassListPreviewer { this.onNewSelection = this.onNewSelection.bind(this); this.onCheckBoxChanged = this.onCheckBoxChanged.bind(this); - this.onKeyPress = this.onKeyPress.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); this.onAddElementInputModified = debounce( this.onAddElementInputModified, 75, this ); this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this); + this.onNodeFrontWillUnset = this.onNodeFrontWillUnset.bind(this); // Create the add class text field. this.addEl = this.doc.createElement("input"); @@ -47,7 +48,7 @@ class ClassListPreviewer { "placeholder", L10N.getStr("inspector.classPanel.newClass.placeholder") ); - this.addEl.addEventListener("keypress", this.onKeyPress); + this.addEl.addEventListener("keydown", this.onKeyDown); this.addEl.addEventListener("input", this.onAddElementInputModified); this.containerEl.appendChild(this.addEl); @@ -68,12 +69,17 @@ class ClassListPreviewer { this.addEl.value = item.label; this.autocompletePopup.hidePopup(); this.autocompletePopup.clearItems(); + this.model.previewClass(item.label); } }, }); // Start listening for interesting events. this.inspector.selection.on("new-node-front", this.onNewSelection); + this.inspector.selection.on( + "node-front-will-unset", + this.onNodeFrontWillUnset + ); this.containerEl.addEventListener("input", this.onCheckBoxChanged); this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged); @@ -82,7 +88,11 @@ class ClassListPreviewer { destroy() { this.inspector.selection.off("new-node-front", this.onNewSelection); - this.addEl.removeEventListener("keypress", this.onKeyPress); + this.inspector.selection.off( + "node-front-will-unset", + this.onNodeFrontWillUnset + ); + this.addEl.removeEventListener("keydown", this.onKeyDown); this.addEl.removeEventListener("input", this.onAddElementInputModified); this.containerEl.removeEventListener("input", this.onCheckBoxChanged); @@ -181,7 +191,7 @@ class ClassListPreviewer { }); } - onKeyPress(event) { + onKeyDown(event) { // If the popup is already open, all the keyboard interaction are handled // directly by the popup component. if (this.autocompletePopup.isOpen) { @@ -205,6 +215,8 @@ class ClassListPreviewer { async onAddElementInputModified() { const newValue = this.addEl.value; + this.model.previewClass(newValue); + // if the input is empty, let's close the popup, if it was open. if (newValue === "") { if (this.autocompletePopup.isOpen) { @@ -218,12 +230,16 @@ class ClassListPreviewer { let items = []; try { const classNames = await this.model.getClassNames(newValue); - items = classNames.map(className => { - return { - preLabel: className.substring(0, newValue.length), - label: className, - }; - }); + items = classNames + .filter( + className => !this.model.previewClasses.split(" ").includes(className) + ) + .map(className => { + return { + preLabel: className.substring(0, newValue.length), + label: className, + }; + }); } catch (e) { // If there was an error while retrieving the classNames, we'll simply NOT show the // popup, which is okay. @@ -262,6 +278,11 @@ class ClassListPreviewer { onCurrentNodeClassChanged() { this.render(); } + + onNodeFrontWillUnset() { + this.model.eraseClassPreview(); + this.addEl.value = ""; + } } module.exports = ClassListPreviewer;