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
This commit is contained in:
Raphael Ferrand 2021-11-18 12:52:22 +00:00
Родитель cdd275e25c
Коммит 702f0ad086
5 изменённых файлов: 96 добавлений и 22 удалений

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

@ -192,6 +192,8 @@ Selection.prototype = {
return; return;
} }
this.emit("node-front-will-unset");
this._isSlotted = isSlotted; this._isSlotted = isSlotted;
this._nodeFront = nodeFront; this._nodeFront = nodeFront;

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

@ -38,6 +38,7 @@ class ClassList {
this.inspector.on("markupmutation", this.onMutations); this.inspector.on("markupmutation", this.onMutations);
this.classListProxyNode = this.inspector.panelDoc.createElement("div"); this.classListProxyNode = this.inspector.panelDoc.createElement("div");
this.previewClasses = "";
} }
destroy() { destroy() {
@ -72,11 +73,13 @@ class ClassList {
if (!CLASSES.has(this.currentNode)) { if (!CLASSES.has(this.currentNode)) {
// Use the proxy node to get a clean list of classes. // Use the proxy node to get a clean list of classes.
this.classListProxyNode.className = this.currentNode.className; this.classListProxyNode.className = this.currentNode.className;
const nodeClasses = [ const nodeClasses = [...new Set([...this.classListProxyNode.classList])]
...new Set([...this.classListProxyNode.classList]), .filter(
].map(name => { className => !this.previewClasses.split(" ").includes(className)
return { name, isApplied: true }; )
}); .map(name => {
return { name, isApplied: true };
});
CLASSES.set(this.currentNode, nodeClasses); CLASSES.set(this.currentNode, nodeClasses);
} }
@ -89,10 +92,15 @@ class ClassList {
* enabled classes are added. * enabled classes are added.
*/ */
get currentClassesPreview() { get currentClassesPreview() {
return this.currentClasses const currentClasses = this.currentClasses
.filter(({ isApplied }) => isApplied) .filter(({ isApplied }) => isApplied)
.map(({ name }) => name) .map(({ name }) => name);
.join(" "); 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) { addClassName(classNameString) {
this.classListProxyNode.className = classNameString; this.classListProxyNode.className = classNameString;
this.eraseClassPreview();
return Promise.all( return Promise.all(
[...new Set([...this.classListProxyNode.classList])].map(name => { [...new Set([...this.classListProxyNode.classList])].map(name => {
return this.addClass(name); return this.addClass(name);
@ -211,6 +220,17 @@ class ClassList {
this.currentNode this.currentNode
); );
} }
previewClass(inputClasses) {
if (this.previewClasses !== inputClasses) {
this.previewClasses = inputClasses;
this.applyClassState();
}
}
eraseClassPreview() {
this.previewClass("");
}
} }
module.exports = ClassList; module.exports = ClassList;

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

@ -70,7 +70,9 @@ add_task(async function() {
info(`Enter the test string in the field: ${textEntered}`); info(`Enter the test string in the field: ${textEntered}`);
for (const key of textEntered.split("")) { for (const key of textEntered.split("")) {
const onPreviewMutation = inspector.once("markupmutation");
EventUtils.synthesizeKey(key, {}, view.styleWindow); EventUtils.synthesizeKey(key, {}, view.styleWindow);
await onPreviewMutation;
} }
info("Submit the change and wait for the textfield to become empty"); info("Submit the change and wait for the textfield to become empty");

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

@ -45,6 +45,7 @@ add_task(async function() {
const { autocompletePopup } = view.classListPreviewer; const { autocompletePopup } = view.classListPreviewer;
let onPopupOpened = autocompletePopup.once("popup-opened"); let onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("a", {}, view.styleWindow); EventUtils.synthesizeKey("a", {}, view.styleWindow);
await waitForClassApplied("a");
await onPopupOpened; await onPopupOpened;
await checkAutocompleteItems( await checkAutocompleteItems(
autocompletePopup, autocompletePopup,
@ -55,17 +56,14 @@ add_task(async function() {
info( info(
"Test that typing more letters filters the autocomplete popup and uses the cache mechanism" "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); EventUtils.sendString("uto-b", view.styleWindow);
await waitForClassApplied("auto-b");
await checkAutocompleteItems( await checkAutocompleteItems(
autocompletePopup, autocompletePopup,
allClasses.filter(cls => cls.startsWith("auto-b")), allClasses.filter(cls => cls.startsWith("auto-b")),
"The autocomplete popup was filtered with the content of the input" "The autocomplete popup was filtered with the content of the input"
); );
await onCacheHit;
ok(true, "The results were retrieved from the cache mechanism"); ok(true, "The results were retrieved from the cache mechanism");
info("Test that autocomplete shows up-to-date results"); 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"); content.document.body.classList.add("auto-body-added-by-script");
}); });
await onNewMutation; await onNewMutation;
await waitForClassApplied("auto-body-added-by-script");
// input is now auto-body // input is now auto-body
onPopupOpened = autocompletePopup.once("popup-opened"); onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.sendString("ody", view.styleWindow); EventUtils.sendString("ody", view.styleWindow);
await waitForClassApplied("auto-body");
await onPopupOpened; await onPopupOpened;
await checkAutocompleteItems( await checkAutocompleteItems(
autocompletePopup, autocompletePopup,
@ -96,6 +96,7 @@ add_task(async function() {
// input is now auto-bodyy // input is now auto-bodyy
let onPopupClosed = autocompletePopup.once("popup-closed"); let onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("y", {}, view.styleWindow); EventUtils.synthesizeKey("y", {}, view.styleWindow);
await waitForClassApplied("auto-bodyy");
await onPopupClosed; await onPopupClosed;
ok(true, "The popup was closed as expected"); ok(true, "The popup was closed as expected");
await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared"); await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared");
@ -108,6 +109,7 @@ add_task(async function() {
onPopupOpened = autocompletePopup.once("popup-opened"); onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("a", {}, view.styleWindow); EventUtils.synthesizeKey("a", {}, view.styleWindow);
await waitForClassApplied("a");
await onPopupOpened; await onPopupOpened;
await checkAutocompleteItems( await checkAutocompleteItems(
@ -147,6 +149,7 @@ add_task(async function() {
onPopupClosed = autocompletePopup.once("popup-closed"); onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow); EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
await waitForClassApplied("auto-body-added-by-script");
await onPopupClosed; await onPopupClosed;
is( is(
textInput.value, textInput.value,
@ -157,6 +160,7 @@ add_task(async function() {
// Backspace to show the list again // Backspace to show the list again
onPopupOpened = autocompletePopup.once("popup-opened"); onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow); EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
await waitForClassApplied("auto-body-added-by-scrip");
await onPopupOpened; await onPopupOpened;
is( is(
textInput.value, textInput.value,
@ -172,6 +176,7 @@ add_task(async function() {
// Enter to accept // Enter to accept
onPopupClosed = autocompletePopup.once("popup-closed"); onPopupClosed = autocompletePopup.once("popup-closed");
EventUtils.synthesizeKey("KEY_Enter", {}, view.styleWindow); EventUtils.synthesizeKey("KEY_Enter", {}, view.styleWindow);
await waitForClassRemoved("auto-body-added-by-scrip");
await onPopupClosed; await onPopupClosed;
is( is(
textInput.value, textInput.value,
@ -182,6 +187,7 @@ add_task(async function() {
// Backspace to show again // Backspace to show again
onPopupOpened = autocompletePopup.once("popup-opened"); onPopupOpened = autocompletePopup.once("popup-opened");
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow); EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
await waitForClassApplied("auto-body-added-by-scrip");
await onPopupOpened; await onPopupOpened;
is( is(
textInput.value, textInput.value,
@ -203,6 +209,7 @@ add_task(async function() {
"auto-body-added-by-script", "auto-body-added-by-script",
"Tab puts the selected item in the input and closes the popup" "Tab puts the selected item in the input and closes the popup"
); );
await waitForClassRemoved("auto-body-added-by-scrip");
}); });
async function checkAutocompleteItems( async function checkAutocompleteItems(
@ -224,3 +231,25 @@ function getAutocompleteItems(autocompletePopup) {
el => el.textContent 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);
}

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

@ -31,13 +31,14 @@ class ClassListPreviewer {
this.onNewSelection = this.onNewSelection.bind(this); this.onNewSelection = this.onNewSelection.bind(this);
this.onCheckBoxChanged = this.onCheckBoxChanged.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 = debounce(
this.onAddElementInputModified, this.onAddElementInputModified,
75, 75,
this this
); );
this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this); this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
this.onNodeFrontWillUnset = this.onNodeFrontWillUnset.bind(this);
// Create the add class text field. // Create the add class text field.
this.addEl = this.doc.createElement("input"); this.addEl = this.doc.createElement("input");
@ -47,7 +48,7 @@ class ClassListPreviewer {
"placeholder", "placeholder",
L10N.getStr("inspector.classPanel.newClass.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.addEl.addEventListener("input", this.onAddElementInputModified);
this.containerEl.appendChild(this.addEl); this.containerEl.appendChild(this.addEl);
@ -68,12 +69,17 @@ class ClassListPreviewer {
this.addEl.value = item.label; this.addEl.value = item.label;
this.autocompletePopup.hidePopup(); this.autocompletePopup.hidePopup();
this.autocompletePopup.clearItems(); this.autocompletePopup.clearItems();
this.model.previewClass(item.label);
} }
}, },
}); });
// Start listening for interesting events. // Start listening for interesting events.
this.inspector.selection.on("new-node-front", this.onNewSelection); 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.containerEl.addEventListener("input", this.onCheckBoxChanged);
this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged); this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
@ -82,7 +88,11 @@ class ClassListPreviewer {
destroy() { destroy() {
this.inspector.selection.off("new-node-front", this.onNewSelection); 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.addEl.removeEventListener("input", this.onAddElementInputModified);
this.containerEl.removeEventListener("input", this.onCheckBoxChanged); 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 // If the popup is already open, all the keyboard interaction are handled
// directly by the popup component. // directly by the popup component.
if (this.autocompletePopup.isOpen) { if (this.autocompletePopup.isOpen) {
@ -205,6 +215,8 @@ class ClassListPreviewer {
async onAddElementInputModified() { async onAddElementInputModified() {
const newValue = this.addEl.value; const newValue = this.addEl.value;
this.model.previewClass(newValue);
// if the input is empty, let's close the popup, if it was open. // if the input is empty, let's close the popup, if it was open.
if (newValue === "") { if (newValue === "") {
if (this.autocompletePopup.isOpen) { if (this.autocompletePopup.isOpen) {
@ -218,12 +230,16 @@ class ClassListPreviewer {
let items = []; let items = [];
try { try {
const classNames = await this.model.getClassNames(newValue); const classNames = await this.model.getClassNames(newValue);
items = classNames.map(className => { items = classNames
return { .filter(
preLabel: className.substring(0, newValue.length), className => !this.model.previewClasses.split(" ").includes(className)
label: className, )
}; .map(className => {
}); return {
preLabel: className.substring(0, newValue.length),
label: className,
};
});
} catch (e) { } catch (e) {
// If there was an error while retrieving the classNames, we'll simply NOT show the // If there was an error while retrieving the classNames, we'll simply NOT show the
// popup, which is okay. // popup, which is okay.
@ -262,6 +278,11 @@ class ClassListPreviewer {
onCurrentNodeClassChanged() { onCurrentNodeClassChanged() {
this.render(); this.render();
} }
onNodeFrontWillUnset() {
this.model.eraseClassPreview();
this.addEl.value = "";
}
} }
module.exports = ClassListPreviewer; module.exports = ClassListPreviewer;