зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
859225f457
Коммит
f69ce054c6
|
@ -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") {
|
||||
|
|
Загрузка…
Ссылка в новой задаче