/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Maximum number of selector suggestions shown in the panel. const MAX_SUGGESTIONS = 15;
/** * Converts any input field into a document search box. * * @param {InspectorPanel} inspector * The InspectorPanel to access the inspector commands for * search and document traversal. * @param {DOMNode} input * The input element to which the panel will be attached and from where * search input will be taken. * @param {DOMNode} clearBtn * The clear button in the input field that will clear the input value. * * Emits the following events: * - search-cleared: when the search box is emptied * - search-result: when a search is made and a result is selected
*/ function InspectorSearch(inspector, input, clearBtn) { this.inspector = inspector; this.searchBox = input; this.searchClearButton = clearBtn; this._lastSearched = null;
/** * Converts any input box on a page to a CSS selector search and suggestion box. * * Emits 'processing-done' event when it is done processing the current * keypress, search request or selection from the list, whether that led to a * search or not. * * @constructor * @param InspectorPanel inspector * The InspectorPanel to access the inspector commands for * search and document traversal. * @param nsiInputElement inputNode * The input element to which the panel will be attached and from where * search input will be taken.
*/ class SelectorAutocompleter extends EventEmitter {
constructor(inspector, inputNode) { super();
// The possible states of the query.
States = { CLASS: "class",
ID: "id",
TAG: "tag",
ATTRIBUTE: "attribute",
};
// The current state of the query.
#state = null;
// The query corresponding to last state computation.
#lastStateCheckAt = null;
get walker() { returnthis.inspector.walker;
}
/** * Computes the state of the query. State refers to whether the query * currently requires a class suggestion, or a tag, or an Id suggestion. * This getter will effectively compute the state by traversing the query * character by character each time the query changes. * * @example * '#f' requires an Id suggestion, so the state is States.ID * 'div > .foo' requires class suggestion, so state is States.CLASS
*/ // eslint-disable-next-line complexity
get state() { if (!this.searchBox || !this.searchBox.value) { returnnull;
}
const query = this.searchBox.value; if (this.#lastStateCheckAt == query) { // If query is the same, return early. returnthis.#state;
} this.#lastStateCheckAt = query;
this.#state = null;
let subQuery = ""; // Now we iterate over the query and decide the state character by // character. // The logic here is that while iterating, the state can go from one to // another with some restrictions. Like, if the state is Class, then it can // never go to Tag state without a space or '>' character; Or like, a Class // state with only '.' cannot go to an Id state without any [a-zA-Z] after // the '.' which means that '.#' is a selector matching a class name '#'. // Similarily for '#.' which means a selctor matching an id '.'. for (let i = 1; i <= query.length; i++) { // Calculate the state.
subQuery = query.slice(0, i);
let [secondLastChar, lastChar] = subQuery.slice(-2); switch (this.#state) { casenull: // This will happen only in the first iteration of the for loop.
lastChar = secondLastChar;
/** * Handles keypresses inside the input box.
*/
#onSearchKeypress = event => { const popup = this.searchPopup; switch (event.keyCode) { case KeyCodes.DOM_VK_RETURN: case KeyCodes.DOM_VK_TAB: if (popup.isOpen) { if (popup.selectedItem) { this.searchBox.value = popup.selectedItem.label;
} this.hidePopup();
} elseif (!popup.isOpen) { // When tab is pressed with focus on searchbox and closed popup, // do not prevent the default to avoid a keyboard trap and move focus // to next/previous element. this.emitForTests("processing-done"); return;
} break;
case KeyCodes.DOM_VK_UP: if (popup.isOpen && popup.itemCount > 0) {
popup.selectPreviousItem(); this.searchBox.value = popup.selectedItem.label;
} break;
case KeyCodes.DOM_VK_DOWN: if (popup.isOpen && popup.itemCount > 0) {
popup.selectNextItem(); this.searchBox.value = popup.selectedItem.label;
} break;
case KeyCodes.DOM_VK_ESCAPE: if (popup.isOpen) { this.hidePopup();
} else { this.emitForTests("processing-done"); return;
} break;
/** * Populates the suggestions list and show the suggestion popup. * * @return {Promise} promise that will resolve when the autocomplete popup is fully * displayed or hidden.
*/
#showPopup(list, popupState) {
let total = 0; const query = this.searchBox.value; const items = [];
for (let [value, , state] of list) { if (query.match(/[\s>+~]$/)) { // for cases like 'div ', 'div >', 'div+' or 'div~'
value = query + value;
} elseif (query.match(/[\s>+~][\.#a-zA-Z][^\s>+~\.#\[]*$/)) { // for cases like 'div #a' or 'div .a' or 'div > d' and likewise const lastPart = query.match(/[\s>+~][\.#a-zA-Z][^\s>+~\.#\[]*$/)[0];
value = query.slice(0, -1 * lastPart.length + 1) + value;
} elseif (query.match(/[a-zA-Z][#\.][^#\.\s+>~]*$/)) { // for cases like 'div.class' or '#foo.bar' and likewise const lastPart = query.match(/[a-zA-Z][#\.][^#\.\s+>~]*$/)[0];
value = query.slice(0, -1 * lastPart.length + 1) + value;
} elseif (query.match(/[a-zA-Z]*\[[^\]]*\][^\]]*/)) { // for cases like '[foo].bar' and likewise const attrPart = query.substring(0, query.lastIndexOf("]") + 1);
value = attrPart + value;
}
const item = {
preLabel: query,
label: value,
};
// In case the query's state is tag and the item's state is id or class // adjust the preLabel if (popupState === this.States.TAG && state === this.States.CLASS) {
item.preLabel = "." + item.preLabel;
} if (popupState === this.States.TAG && state === this.States.ID) {
item.preLabel = "#" + item.preLabel;
}
if (total > 0) { const onPopupOpened = this.searchPopup.once("popup-opened"); this.searchPopup.once("popup-closed", () => { this.searchPopup.setItems(items); // The offset is left padding (22px) + left border width (1px) of searchBox. const xOffset = 23; this.searchPopup.openPopup(this.searchBox, xOffset);
}); this.searchPopup.hidePopup(); return onPopupOpened;
}
returnthis.hidePopup();
}
/** * Hide the suggestion popup if necessary.
*/
hidePopup() { const onPopupClosed = this.searchPopup.once("popup-closed"); this.searchPopup.hidePopup(); return onPopupClosed;
}
/** * Suggests classes,ids and tags based on the user input as user types in the * searchbox.
*/
async showSuggestions() {
let query = this.searchBox.value; const originalQuery = this.searchBox.value;
const state = this.state;
let firstPart = "";
if (query.endsWith("*") || state === this.States.ATTRIBUTE) { // Hide the popup if the query ends with * (because we don't want to // suggest all nodes) or if it is an attribute selector (because // it would give a lot of useless results). this.hidePopup(); this.emitForTests("processing-done", { query: originalQuery }); return;
}
if (state === this.States.TAG) { // gets the tag that is being completed. For ex: // - 'di' returns 'di' // - 'div.foo s' returns 's' // - 'div.foo > s' returns 's' // - 'div.foo + s' returns 's' // - 'div.foo ~ s' returns 's' // - 'div.foo x-el_1' returns 'x-el_1' const matches = query.match(/[\s>+~]?(?<tag>[a-zA-Z0-9_-]*)$/);
firstPart = matches.groups.tag;
query = query.slice(0, query.length - firstPart.length);
} elseif (state === this.States.CLASS) { // gets the class that is being completed. For ex. '.foo.b' returns 'b'
firstPart = query.match(/\.([^\.]*)$/)[1];
query = query.slice(0, query.length - firstPart.length - 1);
} elseif (state === this.States.ID) { // gets the id that is being completed. For ex. '.foo#b' returns 'b'
firstPart = query.match(/#([^#]*)$/)[1];
query = query.slice(0, query.length - firstPart.length - 1);
} // TODO: implement some caching so that over the wire request is not made // everytime. if (/[\s+>~]$/.test(query)) {
query += "*";
}
let suggestions =
await this.inspector.commands.inspectorCommand.getSuggestionsForQuery(
query,
firstPart,
state
);
// If there is a single tag match and it's what the user typed, then // don't need to show a popup. if (suggestions.length === 1 && suggestions[0][0] === firstPart) {
suggestions = [];
}
// Wait for the autocomplete-popup to fire its popup-opened event, to make sure // the autoSelect item has been selected.
await this.#showPopup(suggestions, state); this.emitForTests("processing-done", { query: originalQuery });
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.