Bug 1654731 - [devtools] Preview class when selecting item in the cls autocomplete popup r=jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D132261
This commit is contained in:
Raphael Ferrand 2022-04-25 22:28:35 +00:00
Родитель 859225f457
Коммит f69ce054c6
5 изменённых файлов: 131 добавлений и 49 удалений

Просмотреть файл

@ -38,7 +38,8 @@ class ClassList {
this.inspector.on("markupmutation", this.onMutations);
this.classListProxyNode = this.inspector.panelDoc.createElement("div");
this.previewClasses = "";
this.previewClasses = [];
this.unresolvedStateChanges = [];
}
destroy() {
@ -75,7 +76,12 @@ class ClassList {
this.classListProxyNode.className = this.currentNode.className;
const nodeClasses = [...new Set([...this.classListProxyNode.classList])]
.filter(
className => !this.previewClasses.split(" ").includes(className)
className =>
!this.previewClasses.some(
previewClass =>
previewClass.className === className &&
!previewClass.wasAppliedOnNode
)
)
.map(name => {
return { name, isApplied: true };
@ -96,11 +102,14 @@ class ClassList {
.filter(({ isApplied }) => isApplied)
.map(({ name }) => name);
const previewClasses = this.previewClasses
.split(" ")
.filter(previewClass => !currentClasses.includes(previewClass))
.filter(item => item !== "");
.filter(previewClass => !currentClasses.includes(previewClass.className))
.filter(item => item !== "")
.map(({ className }) => className);
return currentClasses.concat(previewClasses).join(" ");
return currentClasses
.concat(previewClasses)
.join(" ")
.trim();
}
/**
@ -169,12 +178,13 @@ class ClassList {
return Promise.resolve();
}
// Remember which node we changed and the className we applied, so we can filter out
// dom mutations that are caused by us in onMutations.
this.lastStateChange = {
// Remember which node & className we applied until their mutation event is received, so we
// can filter out dom mutations that are caused by us in onMutations, even in situations when
// a new change is applied before that the event of the previous one has been received yet
this.unresolvedStateChanges.push({
node: this.currentNode,
className: this.currentClassesPreview,
};
});
// Apply the change to the node.
const mod = this.currentNode.startModifyingAttributes();
@ -189,16 +199,19 @@ class ClassList {
continue;
}
const isMutationForOurChange =
this.lastStateChange &&
target === this.lastStateChange.node &&
target.className === this.lastStateChange.className;
const isMutationForOurChange = this.unresolvedStateChanges.some(
previousStateChange =>
previousStateChange.node === target &&
previousStateChange.className === target.className
);
if (!isMutationForOurChange) {
CLASSES.delete(target);
if (target === this.currentNode) {
this.emit("current-node-class-changed");
}
} else {
this.removeResolvedStateChanged(target, target.className);
}
}
}
@ -222,8 +235,18 @@ class ClassList {
}
previewClass(inputClasses) {
if (this.previewClasses !== inputClasses) {
this.previewClasses = inputClasses;
if (
this.previewClasses
.map(previewClass => previewClass.className)
.join(" ") !== inputClasses
) {
this.previewClasses = [];
inputClasses.split(" ").forEach(className => {
this.previewClasses.push({
className: className,
wasAppliedOnNode: this.isClassAlreadyApplied(className),
});
});
this.applyClassState();
}
}
@ -231,6 +254,21 @@ class ClassList {
eraseClassPreview() {
this.previewClass("");
}
removeResolvedStateChanged(currentNode, currentClassesPreview) {
this.unresolvedStateChanges.splice(
0,
this.unresolvedStateChanges.findIndex(
previousState =>
previousState.node === currentNode &&
previousState.className === currentClassesPreview
) + 1
);
}
isClassAlreadyApplied(className) {
return this.currentClasses.some(({ name }) => name === className);
}
}
module.exports = ClassList;

Просмотреть файл

@ -14,6 +14,7 @@ add_task(async function() {
await addTab(TEST_URI);
const { inspector, view } = await openRuleView();
const { addEl: textInput } = view.classListPreviewer;
await selectNode("#auto-div-id-3", inspector);
info("Open the class panel");
view.showClassPanel();
@ -45,7 +46,7 @@ add_task(async function() {
const { autocompletePopup } = view.classListPreviewer;
let onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("a", {}, view.styleWindow);
await waitForClassApplied("a");
await waitForClassApplied("auto-body-class-1", "#auto-div-id-3");
await onPopupOpened;
await checkAutocompleteItems(
autocompletePopup,
@ -57,7 +58,7 @@ add_task(async function() {
"Test that typing more letters filters the autocomplete popup and uses the cache mechanism"
);
EventUtils.sendString("uto-b", view.styleWindow);
await waitForClassApplied("auto-b");
await waitForClassApplied("auto-body-class-1", "#auto-div-id-3");
await checkAutocompleteItems(
autocompletePopup,
@ -74,12 +75,16 @@ add_task(async function() {
content.document.body.classList.add("auto-body-added-by-script");
});
await onNewMutation;
await waitForClassApplied("auto-body-added-by-script");
await waitForClassApplied("auto-body-added-by-script", "body");
// close & reopen the autocomplete so it picks up the added to another element while autocomplete was opened
let onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
await onPopupClosed;
// input is now auto-body
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.sendString("ody", view.styleWindow);
await waitForClassApplied("auto-body");
await onPopupOpened;
await checkAutocompleteItems(
autocompletePopup,
@ -94,9 +99,9 @@ add_task(async function() {
"Test that typing a letter that won't match any of the item closes the popup"
);
// input is now auto-bodyy
let onPopupClosed = autocompletePopup.once("popup-closed");
onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("y", {}, view.styleWindow);
await waitForClassApplied("auto-bodyy");
await waitForClassApplied("auto-bodyy", "#auto-div-id-3");
await onPopupClosed;
ok(true, "The popup was closed as expected");
await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared");
@ -109,7 +114,6 @@ add_task(async function() {
onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("a", {}, view.styleWindow);
await waitForClassApplied("a");
await onPopupOpened;
await checkAutocompleteItems(
@ -149,7 +153,7 @@ add_task(async function() {
onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
await waitForClassApplied("auto-body-added-by-script");
await waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
await onPopupClosed;
is(
textInput.value,
@ -160,7 +164,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 waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
await onPopupOpened;
is(
textInput.value,
@ -187,7 +191,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 waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
await onPopupOpened;
is(
textInput.value,
@ -232,13 +236,17 @@ function getAutocompleteItems(autocompletePopup) {
);
}
async function waitForClassApplied(cls) {
async function waitForClassApplied(cls, selector) {
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)
);
});
await SpecialPowers.spawn(
gBrowser.selectedBrowser,
[cls, selector],
async (_cls, _selector) => {
return ContentTaskUtils.waitForCondition(() =>
content.document.querySelector(_selector).classList.contains(_cls)
);
}
);
// Wait for debounced functions to be executed
await wait(200);
}
@ -247,7 +255,10 @@ 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)
() =>
!content.document
.querySelector("#auto-div-id-3")
.classList.contains(_cls)
);
});
// Wait for debounced functions to be executed

Просмотреть файл

@ -36,5 +36,6 @@
<body class="auto-body-class-1 auto-body-class-2 auto-bold">
<div id="auto-div-id-1" class="auto-div-class-1 auto-div-class-2 auto-bold"> the ocean </div>
<div id="auto-div-id-2" class="auto-div-class-1 auto-div-class-2 auto-bold"> roaring </div>
<div id="auto-div-id-3"> ahead </div>
</body>
</html>

Просмотреть файл

@ -39,6 +39,12 @@ class ClassListPreviewer {
);
this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
this.onNodeFrontWillUnset = this.onNodeFrontWillUnset.bind(this);
this.onAutocompleteClassHovered = debounce(
this.onAutocompleteClassHovered,
75,
this
);
this.onAutocompleteClosed = this.onAutocompleteClosed.bind(this);
// Create the add class text field.
this.addEl = this.doc.createElement("input");
@ -61,7 +67,7 @@ class ClassListPreviewer {
this.autocompletePopup = new AutocompletePopup(this.inspector.toolbox.doc, {
listId: "inspector_classListPreviewer_autocompletePopupListBox",
position: "bottom",
autoSelect: false,
autoSelect: true,
useXulWrapper: true,
input: this.addEl,
onClick: (e, item) => {
@ -72,6 +78,11 @@ class ClassListPreviewer {
this.model.previewClass(item.label);
}
},
onSelect: item => {
if (item) {
this.onAutocompleteClassHovered(item?.label);
}
},
});
// Start listening for interesting events.
@ -82,6 +93,7 @@ class ClassListPreviewer {
);
this.containerEl.addEventListener("input", this.onCheckBoxChanged);
this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
this.autocompletePopup.on("popup-closed", this.onAutocompleteClosed);
this.onNewSelection();
}
@ -92,6 +104,7 @@ class ClassListPreviewer {
"node-front-will-unset",
this.onNodeFrontWillUnset
);
this.autocompletePopup.off("popup-closed", this.onAutocompleteClosed);
this.addEl.removeEventListener("keydown", this.onKeyDown);
this.addEl.removeEventListener("input", this.onAddElementInputModified);
this.containerEl.removeEventListener("input", this.onCheckBoxChanged);
@ -215,13 +228,13 @@ 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) {
this.autocompletePopup.hidePopup();
this.autocompletePopup.clearItems();
} else {
this.model.previewClass("");
}
return;
}
@ -230,16 +243,17 @@ class ClassListPreviewer {
let items = [];
try {
const classNames = await this.model.getClassNames(newValue);
items = classNames
.filter(
className => !this.model.previewClasses.split(" ").includes(className)
)
.map(className => {
return {
preLabel: className.substring(0, newValue.length),
label: className,
};
});
if (!this.autocompletePopup.isOpen) {
this._previewClassesBeforeAutocompletion = this.model.previewClasses.map(
previewClass => previewClass.className
);
}
items = classNames.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.
@ -251,7 +265,8 @@ class ClassListPreviewer {
(items.length == 1 && items[0].label === newValue)
) {
this.autocompletePopup.clearItems();
this.autocompletePopup.hidePopup();
await this.autocompletePopup.hidePopup();
this.model.previewClass(newValue);
} else {
this.autocompletePopup.setItems(items);
this.autocompletePopup.openPopup();
@ -283,6 +298,17 @@ class ClassListPreviewer {
this.model.eraseClassPreview();
this.addEl.value = "";
}
onAutocompleteClassHovered(autocompleteItemLabel = "") {
if (this.autocompletePopup.isOpen) {
this.model.previewClass(autocompleteItemLabel);
}
}
onAutocompleteClosed() {
const inputValue = this.addEl.value;
this.model.previewClass(inputValue);
}
}
module.exports = ClassListPreviewer;

Просмотреть файл

@ -1278,7 +1278,8 @@ var PageStyleActor = protocol.ActorClassWithSpec(pageStyleSpec, {
result,
lcSearch,
attributeType,
targetDocument
targetDocument,
node.rawNode
);
this._collectAttributesFromDocumentStyleSheets(
result,
@ -1298,12 +1299,14 @@ var PageStyleActor = protocol.ActorClassWithSpec(pageStyleSpec, {
* @param {String} search: A string to filter attribute value on.
* @param {String} attributeType: The type of attribute we want to retrieve the values.
* @param {Document} targetDocument: The document the search occurs in.
* @param {Node} currentNode: The current element rawNode
*/
_collectAttributesFromDocumentDOM(
result,
search,
attributeType,
targetDocument
targetDocument,
nodeRawNode
) {
// In order to retrieve attributes from DOM elements in the document, we're going to
// do a query on the root node using attributes selector, to directly get the elements
@ -1318,6 +1321,9 @@ var PageStyleActor = protocol.ActorClassWithSpec(pageStyleSpec, {
const matchingElements = targetDocument.querySelectorAll(selector);
for (const element of matchingElements) {
if (element === nodeRawNode) {
return;
}
// For class attribute, we need to add the elements of the classList that match
// the filter string.
if (attributeType === "class") {