зеркало из https://github.com/mozilla/gecko-dev.git
Backed out 3 changesets (bug 1492797) for perma failures on browser_editor_autocomplete_events.js. CLOSED TREE
Backed out changeset 3bfad9588f0b (bug 1492797) Backed out changeset 3ecf0fc44704 (bug 1492797) Backed out changeset a7f6906f69de (bug 1492797)
This commit is contained in:
Родитель
754c06d4eb
Коммит
657d7e2c6b
|
@ -24,15 +24,9 @@ loader.lazyRequireGetter(
|
|||
* PageStyleFront, the front object for the PageStyleActor
|
||||
*/
|
||||
class PageStyleFront extends FrontClassWithSpec(pageStyleSpec) {
|
||||
_attributesCache = new Map();
|
||||
|
||||
constructor(conn, targetFront, parentFront) {
|
||||
super(conn, targetFront, parentFront);
|
||||
this.inspector = this.getParent();
|
||||
|
||||
this._clearAttributesCache = this._clearAttributesCache.bind(this);
|
||||
this.on("stylesheet-updated", this._clearAttributesCache);
|
||||
this.walker.on("new-mutations", this._clearAttributesCache);
|
||||
}
|
||||
|
||||
form(form) {
|
||||
|
@ -82,56 +76,6 @@ class PageStyleFront extends FrontClassWithSpec(pageStyleSpec) {
|
|||
return ret.entries[0];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of existing attribute values in a node document, given an attribute type.
|
||||
*
|
||||
* @param {String} search: A string to filter attribute value on.
|
||||
* @param {String} attributeType: The type of attribute we want to retrieve the values.
|
||||
* @param {Element} node: The element we want to get possible attributes for. This will
|
||||
* be used to get the document where the search is happening.
|
||||
* @returns {Array<String>} An array of strings
|
||||
*/
|
||||
async getAttributesInOwnerDocument(search, attributeType, node) {
|
||||
if (!attributeType) {
|
||||
throw new Error("`type` should not be empty");
|
||||
}
|
||||
|
||||
if (!this._form.traits.getAttributesInOwnerDocument || !search) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lcFilter = search.toLowerCase();
|
||||
|
||||
// If the new filter includes the string that was used on our last trip to the server,
|
||||
// we can filter the cached results instead of calling the server again.
|
||||
if (
|
||||
this._attributesCache &&
|
||||
this._attributesCache.has(attributeType) &&
|
||||
search.startsWith(this._attributesCache.get(attributeType).search)
|
||||
) {
|
||||
const cachedResults = this._attributesCache
|
||||
.get(attributeType)
|
||||
.results.filter(item => item.toLowerCase().startsWith(lcFilter));
|
||||
this.emitForTests(
|
||||
"getAttributesInOwnerDocument-cache-hit",
|
||||
cachedResults
|
||||
);
|
||||
return cachedResults;
|
||||
}
|
||||
|
||||
const results = await super.getAttributesInOwnerDocument(
|
||||
search,
|
||||
attributeType,
|
||||
node
|
||||
);
|
||||
this._attributesCache.set(attributeType, { search, results });
|
||||
return results;
|
||||
}
|
||||
|
||||
_clearAttributesCache() {
|
||||
this._attributesCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
exports.PageStyleFront = PageStyleFront;
|
||||
|
|
|
@ -193,24 +193,6 @@ class ClassList {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available classNames in the document where the current selected node lives:
|
||||
* - the one already used on elements of the document
|
||||
* - the one defined in Stylesheets of the document
|
||||
*
|
||||
* @param {String} filter: A string the classNames should start with (an insensitive
|
||||
* case matching will be done).
|
||||
* @returns {Promise<Array<String>>} A promise that resolves with an array of strings
|
||||
* matching the passed filter.
|
||||
*/
|
||||
getClassNames(filter) {
|
||||
return this.currentNode.inspectorFront.pageStyle.getAttributesInOwnerDocument(
|
||||
filter,
|
||||
"class",
|
||||
this.currentNode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClassList;
|
||||
|
|
|
@ -5,8 +5,6 @@ support-files =
|
|||
doc_blob_stylesheet.html
|
||||
doc_copystyles.css
|
||||
doc_copystyles.html
|
||||
doc_class_panel_autocomplete_stylesheet.css
|
||||
doc_class_panel_autocomplete.html
|
||||
doc_cssom.html
|
||||
doc_custom.html
|
||||
doc_edit_imported_selector.html
|
||||
|
@ -43,7 +41,6 @@ skip-if = !debug && ((os == 'linux' && bits == 64 && os_version == '18.04') || o
|
|||
[browser_rules_authored_override.js]
|
||||
[browser_rules_blob_stylesheet.js]
|
||||
[browser_rules_class_panel_add.js]
|
||||
[browser_rules_class_panel_autocomplete.js]
|
||||
[browser_rules_class_panel_content.js]
|
||||
[browser_rules_class_panel_edit.js]
|
||||
[browser_rules_class_panel_invalid_nodes.js]
|
||||
|
|
|
@ -1,226 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the autocomplete for the class panel input behaves as expected. The test also
|
||||
// checks that we're using the cache to retrieve the data when we can do so, and that the
|
||||
// cache gets cleared, and we're getting data from the server, when there's mutation on
|
||||
// the page.
|
||||
|
||||
const TEST_URI = `${URL_ROOT}doc_class_panel_autocomplete.html`;
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(TEST_URI);
|
||||
const { inspector, view } = await openRuleView();
|
||||
const { addEl: textInput } = view.classListPreviewer;
|
||||
|
||||
info("Open the class panel");
|
||||
view.showClassPanel();
|
||||
|
||||
textInput.focus();
|
||||
|
||||
info("Type a letter and check that the popup has the expected items");
|
||||
const allClasses = [
|
||||
"auto-body-class-1",
|
||||
"auto-body-class-2",
|
||||
"auto-bold",
|
||||
"auto-cssom-primary-color",
|
||||
"auto-div-class-1",
|
||||
"auto-div-class-2",
|
||||
"auto-html-class-1",
|
||||
"auto-html-class-2",
|
||||
"auto-inline-class-1",
|
||||
"auto-inline-class-2",
|
||||
"auto-inline-class-3",
|
||||
"auto-inline-class-4",
|
||||
"auto-inline-class-5",
|
||||
"auto-stylesheet-class-1",
|
||||
"auto-stylesheet-class-2",
|
||||
"auto-stylesheet-class-3",
|
||||
"auto-stylesheet-class-4",
|
||||
"auto-stylesheet-class-5",
|
||||
];
|
||||
|
||||
const { autocompletePopup } = view.classListPreviewer;
|
||||
let onPopupOpened = autocompletePopup.once("popup-opened");
|
||||
EventUtils.synthesizeKey("a", {}, view.styleWindow);
|
||||
await onPopupOpened;
|
||||
await checkAutocompleteItems(
|
||||
autocompletePopup,
|
||||
allClasses,
|
||||
"The autocomplete popup has all the classes used in the DOM and in stylesheets"
|
||||
);
|
||||
|
||||
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 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");
|
||||
// Modify the content page and assert that the new class is displayed in the
|
||||
// autocomplete if the user types a new letter.
|
||||
const onNewMutation = inspector.inspectorFront.walker.once("new-mutations");
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
|
||||
content.document.body.classList.add("auto-body-added-by-script");
|
||||
});
|
||||
await onNewMutation;
|
||||
|
||||
// input is now auto-body
|
||||
onPopupOpened = autocompletePopup.once("popup-opened");
|
||||
EventUtils.sendString("ody", view.styleWindow);
|
||||
await onPopupOpened;
|
||||
await checkAutocompleteItems(
|
||||
autocompletePopup,
|
||||
[
|
||||
...allClasses.filter(cls => cls.startsWith("auto-body")),
|
||||
"auto-body-added-by-script",
|
||||
].sort(),
|
||||
"The autocomplete popup was filtered with the content of the input"
|
||||
);
|
||||
|
||||
info(
|
||||
"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");
|
||||
EventUtils.synthesizeKey("y", {}, view.styleWindow);
|
||||
await onPopupClosed;
|
||||
ok(true, "The popup was closed as expected");
|
||||
await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared");
|
||||
|
||||
info("Clear the input and try to autocomplete again");
|
||||
textInput.select();
|
||||
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
|
||||
// Wait a bit so the debounced function can be executed
|
||||
await wait(200);
|
||||
|
||||
onPopupOpened = autocompletePopup.once("popup-opened");
|
||||
EventUtils.synthesizeKey("a", {}, view.styleWindow);
|
||||
await onPopupOpened;
|
||||
|
||||
await checkAutocompleteItems(
|
||||
autocompletePopup,
|
||||
[...allClasses, "auto-body-added-by-script"].sort(),
|
||||
"The autocomplete popup was updated with the new class added to the DOM"
|
||||
);
|
||||
|
||||
info("Test keyboard shortcut when the popup is displayed");
|
||||
// Escape to hide
|
||||
onPopupClosed = autocompletePopup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
|
||||
await onPopupClosed;
|
||||
ok(true, "The popup was closed when hitting escape");
|
||||
|
||||
// Ctrl + space to show again
|
||||
onPopupOpened = autocompletePopup.once("popup-opened");
|
||||
EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
|
||||
await onPopupOpened;
|
||||
ok(true, "Popup was opened again with Ctrl+Space");
|
||||
await checkAutocompleteItems(
|
||||
autocompletePopup,
|
||||
[...allClasses, "auto-body-added-by-script"].sort()
|
||||
);
|
||||
|
||||
// Arrow left to hide
|
||||
onPopupClosed = autocompletePopup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_ArrowLeft", {}, view.styleWindow);
|
||||
await onPopupClosed;
|
||||
ok(true, "The popup was closed as when hitting ArrowLeft");
|
||||
|
||||
// Arrow right and Ctrl + space to show again, and Arrow Right to accept
|
||||
onPopupOpened = autocompletePopup.once("popup-opened");
|
||||
EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
|
||||
EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
|
||||
await onPopupOpened;
|
||||
|
||||
onPopupClosed = autocompletePopup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
|
||||
await onPopupClosed;
|
||||
is(
|
||||
textInput.value,
|
||||
"auto-body-added-by-script",
|
||||
"ArrowRight puts the selected item in the input and closes the popup"
|
||||
);
|
||||
|
||||
// Backspace to show the list again
|
||||
onPopupOpened = autocompletePopup.once("popup-opened");
|
||||
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
|
||||
await onPopupOpened;
|
||||
is(
|
||||
textInput.value,
|
||||
"auto-body-added-by-scrip",
|
||||
"ArrowRight puts the selected item in the input and closes the popup"
|
||||
);
|
||||
await checkAutocompleteItems(
|
||||
autocompletePopup,
|
||||
["auto-body-added-by-script"],
|
||||
"The autocomplete does show the matching items after hitting backspace"
|
||||
);
|
||||
|
||||
// Enter to accept
|
||||
onPopupClosed = autocompletePopup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_Enter", {}, view.styleWindow);
|
||||
await onPopupClosed;
|
||||
is(
|
||||
textInput.value,
|
||||
"auto-body-added-by-script",
|
||||
"Enter puts the selected item in the input and closes the popup"
|
||||
);
|
||||
|
||||
// Backspace to show again
|
||||
onPopupOpened = autocompletePopup.once("popup-opened");
|
||||
EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
|
||||
await onPopupOpened;
|
||||
is(
|
||||
textInput.value,
|
||||
"auto-body-added-by-scrip",
|
||||
"ArrowRight puts the selected item in the input and closes the popup"
|
||||
);
|
||||
await checkAutocompleteItems(
|
||||
autocompletePopup,
|
||||
["auto-body-added-by-script"],
|
||||
"The autocomplete does show the matching items after hitting backspace"
|
||||
);
|
||||
|
||||
// Tab to accept
|
||||
onPopupClosed = autocompletePopup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_Tab", {}, view.styleWindow);
|
||||
await onPopupClosed;
|
||||
is(
|
||||
textInput.value,
|
||||
"auto-body-added-by-script",
|
||||
"Tab puts the selected item in the input and closes the popup"
|
||||
);
|
||||
});
|
||||
|
||||
async function checkAutocompleteItems(
|
||||
autocompletePopup,
|
||||
expectedItems,
|
||||
assertionMessage
|
||||
) {
|
||||
await waitForSuccess(
|
||||
() =>
|
||||
getAutocompleteItems(autocompletePopup).length === expectedItems.length
|
||||
);
|
||||
const items = getAutocompleteItems(autocompletePopup);
|
||||
const formatList = list => `\n${list.join("\n")}\n`;
|
||||
is(formatList(items), formatList(expectedItems), assertionMessage);
|
||||
}
|
||||
|
||||
function getAutocompleteItems(autocompletePopup) {
|
||||
return Array.from(autocompletePopup._panel.querySelectorAll("li")).map(
|
||||
el => el.textContent
|
||||
);
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<html class="auto-html-class-1 auto-html-class-2 auto-bold">
|
||||
<head>
|
||||
<title>Class panel autocomplete test</title>
|
||||
|
||||
<link href="./doc_class_panel_autocomplete_stylesheet.css" rel="stylesheet" type="text/css">
|
||||
<style>
|
||||
.auto-inline-class-1 {
|
||||
padding: 1em;
|
||||
}
|
||||
.auto-inline-class-2 {
|
||||
padding: 2em;
|
||||
}
|
||||
.auto-inline-class-3 {
|
||||
padding: 3em;
|
||||
}
|
||||
|
||||
.auto-inline-class-1,
|
||||
div.auto-inline-class-2,
|
||||
p:first-of-type.auto-inline-class-3,
|
||||
.auto-inline-class-4 {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
:root .auto-bold .auto-inline-class-5 {
|
||||
font-size: bold;
|
||||
}
|
||||
</style>
|
||||
<script defer>
|
||||
"use strict";
|
||||
const x = document.styleSheets[0];
|
||||
x.insertRule(".auto-cssom-primary-color { color: tomato; }", 1);
|
||||
</script>
|
||||
</head>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
|
@ -1,20 +0,0 @@
|
|||
.auto-stylesheet-class-1 {
|
||||
padding: 1em;
|
||||
}
|
||||
.auto-stylesheet-class-2 {
|
||||
padding: 2em;
|
||||
}
|
||||
.auto-stylesheet-class-3 {
|
||||
padding: 3em;
|
||||
}
|
||||
|
||||
.auto-stylesheet-class-1,
|
||||
div.auto-stylesheet-class-2,
|
||||
p:first-of-type.auto-stylesheet-class-3,
|
||||
.auto-stylesheet-class-4 {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
:root .auto-bold .auto-stylesheet-class-5 {
|
||||
font-size: bold;
|
||||
}
|
|
@ -5,89 +5,61 @@
|
|||
"use strict";
|
||||
|
||||
const ClassList = require("devtools/client/inspector/rules/models/class-list");
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
|
||||
const L10N = new LocalizationHelper(
|
||||
"devtools/client/locales/inspector.properties"
|
||||
);
|
||||
const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
|
||||
const { debounce } = require("devtools/shared/debounce");
|
||||
|
||||
/**
|
||||
* This UI widget shows a textfield and a series of checkboxes in the rule-view. It is
|
||||
* used to toggle classes on the current node selection, and add new classes.
|
||||
*
|
||||
* @param {Inspector} inspector
|
||||
* The current inspector instance.
|
||||
* @param {DomNode} containerEl
|
||||
* The element in the rule-view where the widget should go.
|
||||
*/
|
||||
class ClassListPreviewer {
|
||||
/*
|
||||
* @param {Inspector} inspector
|
||||
* The current inspector instance.
|
||||
* @param {DomNode} containerEl
|
||||
* The element in the rule-view where the widget should go.
|
||||
*/
|
||||
constructor(inspector, containerEl) {
|
||||
this.inspector = inspector;
|
||||
this.containerEl = containerEl;
|
||||
this.model = new ClassList(inspector);
|
||||
function ClassListPreviewer(inspector, containerEl) {
|
||||
this.inspector = inspector;
|
||||
this.containerEl = containerEl;
|
||||
this.model = new ClassList(inspector);
|
||||
|
||||
this.onNewSelection = this.onNewSelection.bind(this);
|
||||
this.onCheckBoxChanged = this.onCheckBoxChanged.bind(this);
|
||||
this.onKeyPress = this.onKeyPress.bind(this);
|
||||
this.onAddElementInputModified = debounce(
|
||||
this.onAddElementInputModified,
|
||||
75,
|
||||
this
|
||||
);
|
||||
this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
|
||||
this.onNewSelection = this.onNewSelection.bind(this);
|
||||
this.onCheckBoxChanged = this.onCheckBoxChanged.bind(this);
|
||||
this.onKeyPress = this.onKeyPress.bind(this);
|
||||
this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
|
||||
|
||||
// Create the add class text field.
|
||||
this.addEl = this.doc.createElement("input");
|
||||
this.addEl.classList.add("devtools-textinput");
|
||||
this.addEl.classList.add("add-class");
|
||||
this.addEl.setAttribute(
|
||||
"placeholder",
|
||||
L10N.getStr("inspector.classPanel.newClass.placeholder")
|
||||
);
|
||||
this.addEl.addEventListener("keypress", this.onKeyPress);
|
||||
this.addEl.addEventListener("input", this.onAddElementInputModified);
|
||||
this.containerEl.appendChild(this.addEl);
|
||||
// Create the add class text field.
|
||||
this.addEl = this.doc.createElement("input");
|
||||
this.addEl.classList.add("devtools-textinput");
|
||||
this.addEl.classList.add("add-class");
|
||||
this.addEl.setAttribute(
|
||||
"placeholder",
|
||||
L10N.getStr("inspector.classPanel.newClass.placeholder")
|
||||
);
|
||||
this.addEl.addEventListener("keypress", this.onKeyPress);
|
||||
this.containerEl.appendChild(this.addEl);
|
||||
|
||||
// Create the class checkboxes container.
|
||||
this.classesEl = this.doc.createElement("div");
|
||||
this.classesEl.classList.add("classes");
|
||||
this.containerEl.appendChild(this.classesEl);
|
||||
// Create the class checkboxes container.
|
||||
this.classesEl = this.doc.createElement("div");
|
||||
this.classesEl.classList.add("classes");
|
||||
this.containerEl.appendChild(this.classesEl);
|
||||
|
||||
// Create the autocomplete popup
|
||||
this.autocompletePopup = new AutocompletePopup(this.inspector.toolbox.doc, {
|
||||
listId: "inspector_classListPreviewer_autocompletePopupListBox",
|
||||
position: "bottom",
|
||||
autoSelect: false,
|
||||
useXulWrapper: true,
|
||||
input: this.addEl,
|
||||
onClick: (e, item) => {
|
||||
if (item) {
|
||||
this.addEl.value = item.label;
|
||||
this.autocompletePopup.hidePopup();
|
||||
this.autocompletePopup.clearItems();
|
||||
}
|
||||
},
|
||||
});
|
||||
// Start listening for interesting events.
|
||||
this.inspector.selection.on("new-node-front", this.onNewSelection);
|
||||
this.containerEl.addEventListener("input", this.onCheckBoxChanged);
|
||||
this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
|
||||
|
||||
// Start listening for interesting events.
|
||||
this.inspector.selection.on("new-node-front", this.onNewSelection);
|
||||
this.containerEl.addEventListener("input", this.onCheckBoxChanged);
|
||||
this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
|
||||
|
||||
this.onNewSelection();
|
||||
}
|
||||
this.onNewSelection();
|
||||
}
|
||||
|
||||
ClassListPreviewer.prototype = {
|
||||
destroy() {
|
||||
this.inspector.selection.off("new-node-front", this.onNewSelection);
|
||||
this.addEl.removeEventListener("keypress", this.onKeyPress);
|
||||
this.addEl.removeEventListener("input", this.onAddElementInputModified);
|
||||
this.containerEl.removeEventListener("input", this.onCheckBoxChanged);
|
||||
|
||||
this.autocompletePopup.destroy();
|
||||
|
||||
this.containerEl.innerHTML = "";
|
||||
|
||||
this.model.destroy();
|
||||
|
@ -95,11 +67,11 @@ class ClassListPreviewer {
|
|||
this.inspector = null;
|
||||
this.addEl = null;
|
||||
this.classesEl = null;
|
||||
}
|
||||
},
|
||||
|
||||
get doc() {
|
||||
return this.containerEl.ownerDocument;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the content of the panel. You typically don't need to call this as the panel
|
||||
|
@ -116,7 +88,7 @@ class ClassListPreviewer {
|
|||
if (!this.model.currentClasses.length) {
|
||||
this.classesEl.appendChild(this.renderNoClassesMessage());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render a single checkbox for a given classname.
|
||||
|
@ -145,7 +117,7 @@ class ClassListPreviewer {
|
|||
labelWrapper.appendChild(label);
|
||||
|
||||
return labelWrapper;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the message displayed in the panel when the current element has no classes.
|
||||
|
@ -157,7 +129,7 @@ class ClassListPreviewer {
|
|||
msg.classList.add("no-classes");
|
||||
msg.textContent = L10N.getStr("inspector.classPanel.noClasses");
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus the add-class text field.
|
||||
|
@ -166,7 +138,7 @@ class ClassListPreviewer {
|
|||
if (this.addEl) {
|
||||
this.addEl.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onCheckBoxChanged({ target }) {
|
||||
if (!target.dataset.name) {
|
||||
|
@ -179,88 +151,34 @@ class ClassListPreviewer {
|
|||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onKeyPress(event) {
|
||||
// If the popup is already open, all the keyboard interaction are handled
|
||||
// directly by the popup component.
|
||||
if (this.autocompletePopup.isOpen) {
|
||||
if (event.key !== "Enter" || this.addEl.value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the autocomplete popup on Ctrl+Space / ArrowDown (when the input isn't empty)
|
||||
if (
|
||||
(this.addEl.value && event.key === " " && event.ctrlKey) ||
|
||||
event.key === "ArrowDown"
|
||||
) {
|
||||
this.onAddElementInputModified();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.addEl.value !== "" && event.key === "Enter") {
|
||||
this.addClassName(this.addEl.value);
|
||||
}
|
||||
}
|
||||
|
||||
async onAddElementInputModified() {
|
||||
const newValue = this.addEl.value;
|
||||
|
||||
// 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();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we need to update the popup items to match the new input.
|
||||
let items = [];
|
||||
try {
|
||||
const classNames = await this.model.getClassNames(newValue);
|
||||
items = classNames.map(className => {
|
||||
return {
|
||||
preLabel: className.substring(0, newValue.length),
|
||||
label: className,
|
||||
};
|
||||
this.model
|
||||
.addClassName(this.addEl.value)
|
||||
.then(() => {
|
||||
this.render();
|
||||
this.addEl.value = "";
|
||||
})
|
||||
.catch(e => {
|
||||
// Only log the error if the panel wasn't destroyed in the meantime.
|
||||
if (this.containerEl) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// If there was an error while retrieving the classNames, we silently bail out;
|
||||
// we'll simply NOT show the popup, which is okay.
|
||||
}
|
||||
|
||||
if (
|
||||
items.length == 0 ||
|
||||
(items.length == 1 && items[0].label === newValue)
|
||||
) {
|
||||
this.autocompletePopup.clearItems();
|
||||
this.autocompletePopup.hidePopup();
|
||||
} else {
|
||||
this.autocompletePopup.setItems(items);
|
||||
this.autocompletePopup.openPopup();
|
||||
}
|
||||
}
|
||||
|
||||
async addClassName(className) {
|
||||
try {
|
||||
await this.model.addClassName(className);
|
||||
this.render();
|
||||
this.addEl.value = "";
|
||||
} catch (e) {
|
||||
// Only log the error if the panel wasn't destroyed in the meantime.
|
||||
if (this.containerEl) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onNewSelection() {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
onCurrentNodeClassChanged() {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ClassListPreviewer;
|
||||
|
|
|
@ -25,7 +25,7 @@ let itemIdCounter = 0;
|
|||
* The toolbox document to attach the autocomplete popup panel.
|
||||
* @param {Object} options
|
||||
* An object consiting any of the following options:
|
||||
* - listId {String} The id for the list <UL> element.
|
||||
* - listId {String} The id for the list <LI> element.
|
||||
* - position {String} The position for the tooltip ("top" or "bottom").
|
||||
* - useXulWrapper {Boolean} If the tooltip is hosted in a XUL document, use a
|
||||
* XUL panel in order to use all the screen viewport available (defaults to false).
|
||||
|
@ -34,9 +34,6 @@ let itemIdCounter = 0;
|
|||
* - onSelect {String} Callback called when the selected index is updated.
|
||||
* - onClick {String} Callback called when the autocomplete popup receives a click
|
||||
* event. The selectedIndex will already be updated if need be.
|
||||
* - input {Element} Optional input element the popup will be bound to. If provided
|
||||
* the event listeners for navigating the autocomplete list are going to be
|
||||
* automatically added.
|
||||
*/
|
||||
function AutocompletePopup(toolboxDoc, options = {}) {
|
||||
EventEmitter.decorate(this);
|
||||
|
@ -58,14 +55,6 @@ function AutocompletePopup(toolboxDoc, options = {}) {
|
|||
this.selectedIndex = -1;
|
||||
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this);
|
||||
this.onInputBlur = this.onInputBlur.bind(this);
|
||||
|
||||
if (options.input) {
|
||||
this.input = options.input;
|
||||
options.input.addEventListener("keydown", this.onInputKeyDown);
|
||||
options.input.addEventListener("blur", this.onInputBlur);
|
||||
}
|
||||
}
|
||||
|
||||
AutocompletePopup.prototype = {
|
||||
|
@ -126,66 +115,6 @@ AutocompletePopup.prototype = {
|
|||
return this._tooltip;
|
||||
},
|
||||
|
||||
onInputKeyDown: function(event) {
|
||||
// Only handle the even if the popup is opened.
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.selectedItem &&
|
||||
this.onClickCallback &&
|
||||
(event.key === "Enter" ||
|
||||
(event.key === "ArrowRight" && !event.shiftKey) ||
|
||||
(event.key === "Tab" && !event.shiftKey))
|
||||
) {
|
||||
this.onClickCallback(event, this.selectedItem);
|
||||
|
||||
// Prevent the associated keypress to be triggered.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the popup when the user hit Left Arrow, but let the keypress be triggered
|
||||
// so the cursor moves as the user wanted.
|
||||
if (event.key === "ArrowLeft" && !event.shiftKey) {
|
||||
this.clearItems();
|
||||
this.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the popup when the user hit Escape.
|
||||
if (event.key === "Escape") {
|
||||
this.clearItems();
|
||||
this.hidePopup();
|
||||
// Prevent the associated keypress to be triggered.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
this.selectNextItem();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
this.selectPreviousItem();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
onInputBlur: function(event) {
|
||||
if (this.isOpen) {
|
||||
this.clearItems();
|
||||
this.hidePopup();
|
||||
}
|
||||
},
|
||||
|
||||
onSelect: function(e) {
|
||||
if (this.onSelectCallback) {
|
||||
this.onSelectCallback(e);
|
||||
|
@ -193,22 +122,14 @@ AutocompletePopup.prototype = {
|
|||
},
|
||||
|
||||
onClick: function(e) {
|
||||
const itemEl = e.target.closest(".autocomplete-item");
|
||||
const index = itemEl?.dataset?.index;
|
||||
|
||||
if (typeof index !== "undefined") {
|
||||
this.selectItemAtIndex(index);
|
||||
const item = e.target.closest(".autocomplete-item");
|
||||
if (item && typeof item.dataset.index !== "undefined") {
|
||||
this.selectItemAtIndex(parseInt(item.dataset.index, 10));
|
||||
}
|
||||
|
||||
this.emit("popup-click");
|
||||
|
||||
const item =
|
||||
typeof index !== "undefined"
|
||||
? this.items[parseInt(itemEl.dataset.index, 10)]
|
||||
: null;
|
||||
|
||||
if (this.onClickCallback) {
|
||||
this.onClickCallback(e, item);
|
||||
this.onClickCallback(e);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -216,7 +137,7 @@ AutocompletePopup.prototype = {
|
|||
* Open the autocomplete popup panel.
|
||||
*
|
||||
* @param {Node} anchor
|
||||
* Optional node to anchor the panel to. Will default to this.input if it exists.
|
||||
* Optional node to anchor the panel to.
|
||||
* @param {Number} xOffset
|
||||
* Horizontal offset in pixels from the left of the node to the left
|
||||
* of the popup.
|
||||
|
@ -228,10 +149,6 @@ AutocompletePopup.prototype = {
|
|||
* @param {Object} options: Check `selectItemAtIndex` for more information.
|
||||
*/
|
||||
openPopup: async function(anchor, xOffset = 0, yOffset = 0, index, options) {
|
||||
if (!anchor && this.input) {
|
||||
anchor = this.input;
|
||||
}
|
||||
|
||||
// Retrieve the anchor's document active element to add accessibility metadata.
|
||||
this._activeElement = anchor.ownerDocument.activeElement;
|
||||
|
||||
|
@ -355,12 +272,6 @@ AutocompletePopup.prototype = {
|
|||
this._tooltip = null;
|
||||
}
|
||||
|
||||
if (this.input) {
|
||||
this.input.addEventListener("keydown", this.onInputKeyDown);
|
||||
this.input.addEventListener("blur", this.onInputBlur);
|
||||
this.input = null;
|
||||
}
|
||||
|
||||
this._document = null;
|
||||
},
|
||||
|
||||
|
|
|
@ -67,7 +67,6 @@ support-files =
|
|||
!/gfx/layers/apz/test/mochitest/apz_test_utils.js
|
||||
|
||||
[browser_autocomplete_popup_consecutive-show.js]
|
||||
[browser_autocomplete_popup_input.js]
|
||||
[browser_autocomplete_popup.js]
|
||||
[browser_browserloader_mocks.js]
|
||||
[browser_css_angle.js]
|
||||
|
|
|
@ -1,241 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function() {
|
||||
const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
|
||||
|
||||
info("Create an autocompletion popup and an input that will be bound to it");
|
||||
const { doc } = await createHost();
|
||||
|
||||
const input = doc.createElement("input");
|
||||
doc.body.appendChild(input);
|
||||
|
||||
const onSelectCalled = [];
|
||||
const onClickCalled = [];
|
||||
const popup = new AutocompletePopup(doc, {
|
||||
input,
|
||||
position: "top",
|
||||
autoSelect: true,
|
||||
onSelect: item => onSelectCalled.push(item),
|
||||
onClick: (e, item) => onClickCalled.push(item),
|
||||
});
|
||||
|
||||
input.focus();
|
||||
|
||||
info(
|
||||
"Check that Tab moves the focus out of the input when the popup isn't opened"
|
||||
);
|
||||
EventUtils.synthesizeKey("KEY_Tab");
|
||||
is(onClickCalled.length, 0, "onClick wasn't called");
|
||||
is(hasFocus(input), false, "input does not have the focus anymore");
|
||||
|
||||
info("Set the focus back to the input and open the popup");
|
||||
input.focus();
|
||||
await populateAndOpenPopup(popup);
|
||||
|
||||
const checkSelectedItem = (expected, info) =>
|
||||
checkPopupSelectedItem(popup, input, expected, info);
|
||||
|
||||
checkSelectedItem(popupItems[0], "First item from top is selected");
|
||||
is(
|
||||
onSelectCalled[0],
|
||||
popupItems[0],
|
||||
"onSelect was called with expected param"
|
||||
);
|
||||
|
||||
info("Check that arrow down/up navigates into the list");
|
||||
EventUtils.synthesizeKey("KEY_ArrowDown");
|
||||
checkSelectedItem(popupItems[1], "item-1 is selected");
|
||||
is(
|
||||
onSelectCalled[1],
|
||||
popupItems[1],
|
||||
"onSelect was called with expected param"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeKey("KEY_ArrowDown");
|
||||
checkSelectedItem(popupItems[2], "item-2 is selected");
|
||||
is(
|
||||
onSelectCalled[2],
|
||||
popupItems[2],
|
||||
"onSelect was called with expected param"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeKey("KEY_ArrowDown");
|
||||
checkSelectedItem(popupItems[0], "item-0 is selected");
|
||||
is(
|
||||
onSelectCalled[3],
|
||||
popupItems[0],
|
||||
"onSelect was called with expected param"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeKey("KEY_ArrowUp");
|
||||
checkSelectedItem(popupItems[2], "item-2 is selected");
|
||||
is(
|
||||
onSelectCalled[4],
|
||||
popupItems[2],
|
||||
"onSelect was called with expected param"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeKey("KEY_ArrowUp");
|
||||
checkSelectedItem(popupItems[1], "item-2 is selected");
|
||||
is(
|
||||
onSelectCalled[5],
|
||||
popupItems[1],
|
||||
"onSelect was called with expected param"
|
||||
);
|
||||
|
||||
info("Check that Escape closes the popup");
|
||||
let onPopupClosed = popup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_Escape");
|
||||
await onPopupClosed;
|
||||
ok(true, "popup was closed with Escape key");
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
is(onClickCalled.length, 0, "onClick wasn't called");
|
||||
|
||||
info("Fill the input");
|
||||
const value = "item";
|
||||
EventUtils.sendString(value);
|
||||
is(input.value, value, "input has the expected value");
|
||||
is(
|
||||
input.selectionStart,
|
||||
value.length,
|
||||
"input cursor is at expected position"
|
||||
);
|
||||
info("Open the popup again");
|
||||
await populateAndOpenPopup(popup);
|
||||
|
||||
info("Check that Arrow Left + Shift does not close the popup");
|
||||
const timeoutRes = "TIMED_OUT";
|
||||
const onRaceEnded = Promise.race([
|
||||
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||
await new Promise(res => setTimeout(() => res(timeoutRes), 500)),
|
||||
popup.once("popup-closed"),
|
||||
]);
|
||||
EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
|
||||
const raceResult = await onRaceEnded;
|
||||
is(raceResult, timeoutRes, "popup wasn't closed");
|
||||
ok(popup.isOpen, "popup is still open");
|
||||
is(input.selectionEnd - input.selectionStart, 1, "text was selected");
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
|
||||
info("Check that Arrow Left closes the popup");
|
||||
onPopupClosed = popup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_ArrowLeft");
|
||||
await onPopupClosed;
|
||||
is(
|
||||
input.selectionStart,
|
||||
value.length - 1,
|
||||
"input cursor was moved one char back"
|
||||
);
|
||||
is(input.selectionEnd, input.selectionStart, "selection was removed");
|
||||
is(onClickCalled.length, 0, "onClick wasn't called");
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
|
||||
info("Open the popup again");
|
||||
await populateAndOpenPopup(popup);
|
||||
|
||||
info("Check that Arrow Right + Shift does not trigger onClick");
|
||||
EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
|
||||
is(onClickCalled.length, 0, "onClick wasn't called");
|
||||
is(input.selectionEnd - input.selectionStart, 1, "input text was selected");
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
|
||||
info("Check that Arrow Right triggers onClick");
|
||||
EventUtils.synthesizeKey("KEY_ArrowRight");
|
||||
is(onClickCalled.length, 1, "onClick was called");
|
||||
is(
|
||||
onClickCalled[0],
|
||||
popupItems[0],
|
||||
"onClick was called with the selected item"
|
||||
);
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
|
||||
info("Check that Enter triggers onClick");
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
is(onClickCalled.length, 2, "onClick was called");
|
||||
is(
|
||||
onClickCalled[1],
|
||||
popupItems[0],
|
||||
"onClick was called with the selected item"
|
||||
);
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
|
||||
info("Check that Tab triggers onClick");
|
||||
EventUtils.synthesizeKey("KEY_Tab");
|
||||
is(onClickCalled.length, 3, "onClick was called");
|
||||
is(
|
||||
onClickCalled[2],
|
||||
popupItems[0],
|
||||
"onClick was called with the selected item"
|
||||
);
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
|
||||
info(
|
||||
"Check that Shift+Tab does not trigger onClick and move the focus out of the input"
|
||||
);
|
||||
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
|
||||
is(onClickCalled.length, 3, "onClick wasn't called");
|
||||
is(hasFocus(input), false, "input does not have the focus anymore");
|
||||
|
||||
const onPopupClose = popup.once("popup-closed");
|
||||
popup.hidePopup();
|
||||
await onPopupClose;
|
||||
});
|
||||
|
||||
const popupItems = [
|
||||
{ label: "item-0", value: "value-0" },
|
||||
{ label: "item-1", value: "value-1" },
|
||||
{ label: "item-2", value: "value-2" },
|
||||
];
|
||||
|
||||
function populateAndOpenPopup(popup) {
|
||||
popup.setItems(popupItems);
|
||||
|
||||
info("Open popup");
|
||||
const onPopupOpen = popup.once("popup-opened");
|
||||
popup.openPopup();
|
||||
return onPopupOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the give node is currently focused.
|
||||
*/
|
||||
function hasFocus(node) {
|
||||
return (
|
||||
node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} popup
|
||||
* @param {*} input
|
||||
* @param {*} expectedSelectedItem
|
||||
* @param {*} info
|
||||
*/
|
||||
function checkPopupSelectedItem(popup, input, expectedSelectedItem, info) {
|
||||
is(popup.selectedItem, expectedSelectedItem, info);
|
||||
checkActiveDescendant(popup, input);
|
||||
ok(hasFocus(input), "input still has the focus");
|
||||
}
|
||||
|
||||
function checkActiveDescendant(popup, input) {
|
||||
const activeElement = input.ownerDocument.activeElement;
|
||||
const descendantId = activeElement.getAttribute("aria-activedescendant");
|
||||
const popupItem = popup._tooltip.panel.querySelector("#" + descendantId);
|
||||
const cloneItem = input.ownerDocument.querySelector("#" + descendantId);
|
||||
|
||||
ok(popupItem, "Active descendant is found in the popup list");
|
||||
ok(cloneItem, "Active descendant is found in the list clone");
|
||||
is(
|
||||
stripNS(popupItem.outerHTML),
|
||||
cloneItem.outerHTML,
|
||||
"Cloned item has the same HTML as the original element"
|
||||
);
|
||||
}
|
||||
|
||||
function stripNS(text) {
|
||||
return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");
|
||||
}
|
|
@ -202,8 +202,6 @@ var PageStyleActor = protocol.ActorClassWithSpec(pageStyleSpec, {
|
|||
// expected support of font-stretch at CSS Fonts Level 4.
|
||||
fontWeightLevel4:
|
||||
CSS.supports("font-weight: 1") && CSS.supports("font-stretch: 100%"),
|
||||
// Introduced in Firefox 80.
|
||||
getAttributesInOwnerDocument: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -1202,171 +1200,6 @@ var PageStyleActor = protocol.ActorClassWithSpec(pageStyleSpec, {
|
|||
rule.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an array of existing attribute values in a node document.
|
||||
*
|
||||
* @param {String} search: A string to filter attribute value on.
|
||||
* @param {String} attributeType: The type of attribute we want to retrieve the values.
|
||||
* @param {Element} node: The element we want to get possible attributes for. This will
|
||||
* be used to get the document where the search is happening.
|
||||
* @returns {Array<String>} An array of strings
|
||||
*/
|
||||
getAttributesInOwnerDocument(search, attributeType, node) {
|
||||
if (!search) {
|
||||
throw new Error("search is mandatory");
|
||||
}
|
||||
|
||||
// In a non-fission world, a node from an iframe shares the same `rootNode` as a node
|
||||
// in the top-level document. So here we need to retrieve the document from the node
|
||||
// in parameter in order to retrieve the right document.
|
||||
// This may change once we have a dedicated walker for every target in a tab, as we'll
|
||||
// be able to directly talk to the "right" walker actor.
|
||||
const targetDocument = node.rawNode.ownerDocument;
|
||||
|
||||
// We store the result in a Set which will contain the attribute value
|
||||
const result = new Set();
|
||||
const lcSearch = search.toLowerCase();
|
||||
this._collectAttributesFromDocumentDOM(
|
||||
result,
|
||||
lcSearch,
|
||||
attributeType,
|
||||
targetDocument
|
||||
);
|
||||
this._collectAttributesFromDocumentStyleSheets(
|
||||
result,
|
||||
lcSearch,
|
||||
attributeType,
|
||||
targetDocument
|
||||
);
|
||||
|
||||
return Array.from(result).sort();
|
||||
},
|
||||
|
||||
/**
|
||||
* Collect attribute values from the document DOM tree, matching the passed filter and
|
||||
* type, to the result Set.
|
||||
*
|
||||
* @param {Set<String>} result: A Set to which the results will be added.
|
||||
* @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.
|
||||
*/
|
||||
_collectAttributesFromDocumentDOM(
|
||||
result,
|
||||
search,
|
||||
attributeType,
|
||||
targetDocument
|
||||
) {
|
||||
// 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
|
||||
// matching the attributes we're looking for.
|
||||
|
||||
// For classes, we need something a bit different as the className we're looking
|
||||
// for might not be the first in the attribute value, meaning we can't use the
|
||||
// "attribute starts with X" selector.
|
||||
const attributeSelectorPositionChar = attributeType === "class" ? "*" : "^";
|
||||
const selector = `[${attributeType}${attributeSelectorPositionChar}=${search} i]`;
|
||||
|
||||
const matchingElements = targetDocument.querySelectorAll(selector);
|
||||
|
||||
for (const element of matchingElements) {
|
||||
// For class attribute, we need to add the elements of the classList that match
|
||||
// the filter string.
|
||||
if (attributeType === "class") {
|
||||
for (const cls of element.classList) {
|
||||
if (!result.has(cls) && cls.toLowerCase().startsWith(search)) {
|
||||
result.add(cls);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { value } = element.attributes[attributeType];
|
||||
// For other attributes, we can directly use the attribute value.
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Collect attribute values from the document stylesheets, matching the passed filter
|
||||
* and type, to the result Set.
|
||||
*
|
||||
* @param {Set<String>} result: A Set to which the results will be added.
|
||||
* @param {String} search: A string to filter attribute value on.
|
||||
* @param {String} attributeType: The type of attribute we want to retrieve the values.
|
||||
* It only supports "class" and "id" at the moment.
|
||||
* @param {Document} targetDocument: The document the search occurs in.
|
||||
*/
|
||||
_collectAttributesFromDocumentStyleSheets(
|
||||
result,
|
||||
search,
|
||||
attributeType,
|
||||
targetDocument
|
||||
) {
|
||||
if (attributeType !== "class" && attributeType !== "id") {
|
||||
return;
|
||||
}
|
||||
|
||||
// We loop through all the stylesheets and their rules, and then use the lexer to only
|
||||
// get the attributes we're looking for.
|
||||
for (const styleSheet of targetDocument.styleSheets) {
|
||||
for (const rule of styleSheet.rules) {
|
||||
this._collectAttributesFromRule(result, rule, search, attributeType);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Collect attribute values from the rule, matching the passed filter and type, to the
|
||||
* result Set.
|
||||
*
|
||||
* @param {Set<String>} result: A Set to which the results will be added.
|
||||
* @param {Rule} rule: The rule the search occurs in.
|
||||
* @param {String} search: A string to filter attribute value on.
|
||||
* @param {String} attributeType: The type of attribute we want to retrieve the values.
|
||||
* It only supports "class" and "id" at the moment.
|
||||
*/
|
||||
_collectAttributesFromRule(result, rule, search, attributeType) {
|
||||
const shouldRetrieveClasses = attributeType === "class";
|
||||
const shouldRetrieveIds = attributeType === "id";
|
||||
|
||||
const { selectorText } = rule;
|
||||
// If there's no selectorText, or if the selectorText does not include the
|
||||
// filter, we can bail out.
|
||||
if (!selectorText || !selectorText.toLowerCase().includes(search)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we should parse the selectorText (do we need to check for class/id and
|
||||
// if so, does the selector contains class/id related chars).
|
||||
const parseForClasses =
|
||||
shouldRetrieveClasses &&
|
||||
selectorText.toLowerCase().includes(`.${search}`);
|
||||
const parseForIds =
|
||||
shouldRetrieveIds && selectorText.toLowerCase().includes(`#${search}`);
|
||||
|
||||
if (!parseForClasses && !parseForIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lexer = getCSSLexer(selectorText);
|
||||
let token;
|
||||
while ((token = lexer.nextToken())) {
|
||||
if (
|
||||
token.tokenType === "symbol" &&
|
||||
((shouldRetrieveClasses && token.text === ".") ||
|
||||
(shouldRetrieveIds && token.text === "#"))
|
||||
) {
|
||||
token = lexer.nextToken();
|
||||
if (
|
||||
token.tokenType === "ident" &&
|
||||
token.text.toLowerCase().startsWith(search)
|
||||
) {
|
||||
result.add(token.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
exports.PageStyleActor = PageStyleActor;
|
||||
|
||||
|
|
|
@ -188,16 +188,6 @@ const pageStyleSpec = generateActorSpec({
|
|||
},
|
||||
response: RetVal("appliedStylesReturn"),
|
||||
},
|
||||
getAttributesInOwnerDocument: {
|
||||
request: {
|
||||
search: Arg(0, "string"),
|
||||
attributeType: Arg(1, "string"),
|
||||
node: Arg(2, "nullable:domnode"),
|
||||
},
|
||||
response: {
|
||||
attributes: RetVal("array:string"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче