commit 8684c3bcd0ad2ff392214d702d8bb48fe556a820 parent b1de292b12f40009febf2cb7bdd02c9f47e1828f Author: iulian moraru <imoraru@mozilla.com> Date: Fri, 2 Jan 2026 18:52:28 +0200 Revert "Bug 2003063, Bug 2007032 - Part 5: Add initial chrome tests for multiline editor r=mak" for causing bc failures on browser_glean_metrics_exist.js. This reverts commit b86e7a7cafacd7f5aae51876bc7d317ac68cade0. Revert "Bug 2003063 - Part 4: Add basic multiline editor r=mak,ai-frontend-reviewers,fluent-reviewers,flod" This reverts commit 3cc26ddf9c172988714dec737d1ccb733c7eb632. Revert "Bug 2003063 - Part 3: Add smartbar to sidebar for dev r=mak,ai-frontend-reviewers" This reverts commit da2a9dd9348b7238737d9f7d21b43c84ef7a2191. Revert "Bug 2003063 - Part 2: Add a new mode to SmartBar r=mak,frontend-codestyle-reviewers,urlbar-reviewers,ai-frontend-reviewers,Standard8" This reverts commit 5a1d7888723db1ae84f3c8cbb758cb97092a2929. Revert "Bug 2003063 - Part 1: Fork UrlbarInput into SmartbarInput r=mak,urlbar-reviewers" This reverts commit ab9720d3d57b3d75da4ec17729036d70770cf682. Diffstat:
29 files changed, 22 insertions(+), 7760 deletions(-)
diff --git a/browser/base/content/browser-main.js b/browser/base/content/browser-main.js @@ -32,9 +32,6 @@ Services.scriptloader.loadSubScript("chrome://browser/content/places/places-menupopup.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this); - if (AIWindow.isOpeningAIWindow(window)) { - ChromeUtils.importESModule("chrome://browser/content/urlbar/SmartbarInput.mjs", { global: "current" }); - } ChromeUtils.importESModule("chrome://browser/content/urlbar/UrlbarInput.mjs", { global: "current" }); } diff --git a/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs b/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs @@ -102,7 +102,7 @@ export const AIWindow = { /** * Is current window an AI Window * - * @param {Window} win current Window + * @param {object} win current Window * @returns {boolean} whether current Window is an AI Window */ isAIWindowActive(win) { @@ -123,21 +123,6 @@ export const AIWindow = { }, /** - * Check if window is being opened as an AI Window. - * - * @param {Window} win - The window to check - * @returns {boolean} whether the window is being opened as an AI Window - */ - isOpeningAIWindow(win) { - const windowArgs = win?.arguments?.[1]; - if (!(windowArgs instanceof Ci.nsIPropertyBag2)) { - return false; - } - - return windowArgs.hasKey("ai-window"); - }, - - /** * Is AI Window content page active * * @param {nsIURI} uri current URI diff --git a/browser/components/genai/content/smart-assist.mjs b/browser/components/genai/content/smart-assist.mjs @@ -277,16 +277,18 @@ export class SmartAssist extends MozLitElement { */ _getOrCreateBrowser(chromeDoc, box) { - let stack = box.querySelector(".ai-window-browser-stack"); - if (!stack) { - stack = chromeDoc.createXULElement("stack"); + // Find existing browser, or create it the first time we open the sidebar. + let browser = chromeDoc.getElementById("ai-window-browser"); + + if (!browser) { + const stack = + box.querySelector(".ai-window-browser-stack") || + chromeDoc.createXULElement("stack"); + stack.className = "ai-window-browser-stack"; stack.setAttribute("flex", "1"); box.appendChild(stack); - } - let browser = stack.querySelector("#ai-window-browser"); - if (!browser) { browser = chromeDoc.createXULElement("browser"); browser.setAttribute("id", "ai-window-browser"); browser.setAttribute("flex", "1"); @@ -301,29 +303,6 @@ export class SmartAssist extends MozLitElement { stack.appendChild(browser); } - return stack; - } - - /** - * Helper method to get or create the smartbar element - * - * @param {Document} chromeDoc - The chrome document - * @param {Element} container - The container element - */ - _getOrCreateSmartbar(chromeDoc, container) { - // Find existing Smartbar, or create it the first time we open the sidebar. - let smartbar = chromeDoc.getElementById("ai-window-smartbar"); - - if (!smartbar) { - smartbar = chromeDoc.createElement("moz-smartbar"); - smartbar.id = "ai-window-smartbar"; - smartbar.setAttribute("sap-name", "smartbar"); - smartbar.setAttribute("pageproxystate", "invalid"); - smartbar.setAttribute("popover", "manual"); - smartbar.classList.add("smartbar", "urlbar"); - container.append(smartbar); - } - return smartbar; } _toggleAIWindowSidebar() { @@ -335,8 +314,7 @@ export class SmartAssist extends MozLitElement { return; } - const stack = this._getOrCreateBrowser(chromeDoc, box); - this._getOrCreateSmartbar(chromeDoc, stack); + this._getOrCreateBrowser(chromeDoc, box); // Toggle visibility const opening = box.hidden; diff --git a/browser/components/moz.build b/browser/components/moz.build @@ -47,7 +47,6 @@ DIRS += [ "messagepreview", "migration", "mozcachedohttp", - "multilineeditor", "newtab", "originattributes", "pagedata", diff --git a/browser/components/multilineeditor/jar.mn b/browser/components/multilineeditor/jar.mn @@ -1,9 +0,0 @@ -# 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/. - -browser.jar: - content/browser/multilineeditor/multiline-editor.css - content/browser/multilineeditor/multiline-editor.mjs - content/browser/multilineeditor/prosemirror.bundle.mjs (../../../third_party/js/prosemirror/prosemirror.bundle.mjs) - content/browser/multilineeditor/prosemirror.css (../../../third_party/js/prosemirror/prosemirror-view/style/prosemirror.css) diff --git a/browser/components/multilineeditor/moz.build b/browser/components/multilineeditor/moz.build @@ -1,10 +0,0 @@ -# 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/. - -with Files("**"): - BUG_COMPONENT = ("Firefox", "Address Bar") - -JAR_MANIFESTS += ["jar.mn"] - -TEST_DIRS += ["tests"] diff --git a/browser/components/multilineeditor/multiline-editor.css b/browser/components/multilineeditor/multiline-editor.css @@ -1,39 +0,0 @@ -/* 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/. */ - -:host { - display: inline-flex; - width: 100%; -} - -.multiline-editor { - flex: 1; -} - -.multiline-editor .ProseMirror { - height: 100%; -} - -.multiline-editor .ProseMirror:focus { - outline: none; -} - -.multiline-editor .ProseMirror p { - margin: 0; - padding: 0; -} - -/* Force empty paragraphs to not collapse */ -.multiline-editor .ProseMirror p BR.ProseMirror-trailingBreak, -.multiline-editor .ProseMirror p br.ProseMirror-trailingBreak { - display: inline-flex; -} - -.multiline-editor .ProseMirror p.placeholder::before { - content: attr(data-placeholder); - position: absolute; - pointer-events: none; - color: var(--text-color-deemphasized); - user-select: none; -} diff --git a/browser/components/multilineeditor/multiline-editor.mjs b/browser/components/multilineeditor/multiline-editor.mjs @@ -1,553 +0,0 @@ -/* 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/. */ - -import { html } from "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; -import { - Decoration, - DecorationSet, - EditorState, - EditorView, - Plugin as PmPlugin, - TextSelection, - baseKeymap, - basicSchema, - history as historyPlugin, - keymap, - redo as historyRedo, - undo as historyUndo, -} from "chrome://browser/content/multilineeditor/prosemirror.bundle.mjs"; - -/** - * @class MultilineEditor - * - * A ProseMirror-based multiline editor. - * - * @property {string} placeholder - Placeholder text for the editor. - * @property {boolean} readOnly - Whether the editor is read-only. - */ -export class MultilineEditor extends MozLitElement { - static shadowRootOptions = { - ...MozLitElement.shadowRootOptions, - delegatesFocus: true, - }; - - static properties = { - placeholder: { type: String, reflect: true, fluent: true }, - readOnly: { type: Boolean, reflect: true, attribute: "readonly" }, - }; - - static schema = basicSchema; - - #pendingValue = ""; - #placeholderPlugin; - #plugins; - #suppressInputEvent = false; - #view; - - constructor() { - super(); - - this.placeholder = ""; - this.readOnly = false; - this.#placeholderPlugin = this.#createPlaceholderPlugin(); - const plugins = [ - historyPlugin(), - keymap({ - Enter: () => true, - "Shift-Enter": (state, dispatch) => - this.#insertParagraph(state, dispatch), - "Mod-z": historyUndo, - "Mod-y": historyRedo, - "Shift-Mod-z": historyRedo, - }), - keymap(baseKeymap), - this.#placeholderPlugin, - ]; - - if (document.contentType === "application/xhtml+xml") { - plugins.push(this.#createCleanupOrphanedBreaksPlugin()); - } - - this.#plugins = plugins; - } - - /** - * Whether the editor is composing. - * - * @type {boolean} - */ - get composing() { - return this.#view?.composing ?? false; - } - - /** - * The current text content of the editor. - * - * @type {string} - */ - get value() { - if (!this.#view) { - return this.#pendingValue; - } - return this.#view.state.doc.textBetween( - 0, - this.#view.state.doc.content.size, - "\n", - "\n" - ); - } - - /** - * Set the text content of the editor. - * - * @param {string} val - */ - set value(val) { - if (!this.#view) { - this.#pendingValue = val; - return; - } - - if (val === this.value) { - return; - } - - const state = this.#view.state; - const schema = state.schema; - const lines = val.split("\n"); - const paragraphs = lines.map(line => { - const content = line ? [schema.text(line)] : []; - return schema.node("paragraph", null, content); - }); - const doc = schema.node("doc", null, paragraphs); - - const tr = state.tr.replaceWith(0, state.doc.content.size, doc.content); - tr.setMeta("addToHistory", false); - - const cursorPos = this.#posFromTextOffset(val.length, tr.doc); - // Suppress input events when updating only the text selection. - this.#suppressInputEvent = true; - try { - this.#view.dispatch( - tr.setSelection( - TextSelection.between( - tr.doc.resolve(cursorPos), - tr.doc.resolve(cursorPos) - ) - ) - ); - } finally { - this.#suppressInputEvent = false; - } - } - - /** - * The start offset of the selection. - * - * @type {number} - */ - get selectionStart() { - if (!this.#view) { - return 0; - } - return this.#textOffsetFromPos(this.#view.state.selection.from); - } - - /** - * Set the start offset of the selection. - * - * @param {number} val - */ - set selectionStart(val) { - this.setSelectionRange(val, this.selectionEnd ?? val); - } - - /** - * The end offset of the selection. - * - * @type {number} - */ - get selectionEnd() { - if (!this.#view) { - return 0; - } - return this.#textOffsetFromPos(this.#view.state.selection.to); - } - - /** - * Set the end offset of the selection. - * - * @param {number} val - */ - set selectionEnd(val) { - this.setSelectionRange(this.selectionStart ?? 0, val); - } - - /** - * Set the selection range in the editor. - * - * @param {number} start - * @param {number} end - */ - setSelectionRange(start, end) { - if (!this.#view) { - return; - } - - const doc = this.#view.state.doc; - const docSize = doc.content.size; - const maxOffset = this.#textLength(doc); - const fromOffset = Math.max(0, Math.min(start ?? 0, maxOffset)); - const toOffset = Math.max(0, Math.min(end ?? fromOffset, maxOffset)); - const from = Math.max( - 0, - Math.min(this.#posFromTextOffset(fromOffset, doc), docSize) - ); - const to = Math.max( - 0, - Math.min(this.#posFromTextOffset(toOffset, doc), docSize) - ); - - if ( - this.#view.state.selection.from === from && - this.#view.state.selection.to === to - ) { - return; - } - - let selection; - try { - selection = TextSelection.between(doc.resolve(from), doc.resolve(to)); - } catch (_e) { - const anchor = Math.max(0, Math.min(to, docSize)); - selection = TextSelection.near(doc.resolve(anchor)); - } - this.#view.dispatch( - this.#view.state.tr.setSelection(selection).scrollIntoView() - ); - this.#dispatchSelectionChange(); - } - - /** - * Select all text in the editor. - */ - select() { - this.setSelectionRange(0, this.value.length); - } - - /** - * Focus the editor. - */ - focus() { - this.#view?.focus(); - super.focus(); - } - - /** - * Called when the element is added to the DOM. - */ - connectedCallback() { - super.connectedCallback(); - this.setAttribute("role", "presentation"); - } - - /** - * Called when the element is removed from the DOM. - */ - disconnectedCallback() { - this.#destroyView(); - this.#pendingValue = ""; - super.disconnectedCallback(); - } - - /** - * Called after the element’s DOM has been rendered for the first time. - */ - firstUpdated() { - this.#createView(); - } - - /** - * Called when the element’s properties are updated. - * - * @param {Map} changedProps - */ - updated(changedProps) { - if (changedProps.has("placeholder") || changedProps.has("readOnly")) { - this.#refreshView(); - } - } - - #createView() { - const mount = this.renderRoot.querySelector(".multiline-editor"); - if (!mount) { - return; - } - - const state = EditorState.create({ - schema: MultilineEditor.schema, - plugins: this.#plugins, - }); - - this.#view = new EditorView(mount, { - state, - attributes: this.#viewAttributes(), - editable: () => !this.readOnly, - dispatchTransaction: this.#dispatchTransaction, - }); - - if (this.#pendingValue) { - this.value = this.#pendingValue; - this.#pendingValue = ""; - } - } - - #destroyView() { - this.#view?.destroy(); - this.#view = null; - } - - #dispatchTransaction = tr => { - if (!this.#view) { - return; - } - - const prevText = this.value; - const prevSelection = this.#view.state.selection; - const nextState = this.#view.state.apply(tr); - this.#view.updateState(nextState); - - const selectionChanged = - tr.selectionSet && - (prevSelection.from !== nextState.selection.from || - prevSelection.to !== nextState.selection.to); - - if (selectionChanged) { - this.#dispatchSelectionChange(); - } - - if (tr.docChanged && !this.#suppressInputEvent) { - const nextText = this.value; - let insertedText = ""; - for (const step of tr.steps) { - insertedText += step.slice?.content?.textBetween( - 0, - step.slice.content.size, - "", - "" - ); - } - this.dispatchEvent( - new InputEvent("input", { - bubbles: true, - composed: true, - data: insertedText || null, - inputType: - insertedText || nextText.length >= prevText.length - ? "insertText" - : "deleteContentBackward", - }) - ); - } - }; - - #dispatchSelectionChange() { - this.dispatchEvent( - new Event("selectionchange", { bubbles: true, composed: true }) - ); - } - - #insertParagraph(state, dispatch) { - const paragraph = state.schema.nodes.paragraph; - if (!paragraph) { - return false; - } - const { $from } = state.selection; - let tr = state.tr; - if (!state.selection.empty) { - tr = tr.deleteSelection(); - } - tr = tr.split(tr.mapping.map($from.pos)).scrollIntoView(); - dispatch(tr); - return true; - } - - /** - * Creates a plugin that shows a placeholder when the editor is empty. - * - * @returns {PmPlugin} - */ - #createPlaceholderPlugin() { - return new PmPlugin({ - props: { - decorations: ({ doc }) => { - if ( - doc.childCount !== 1 || - !doc.firstChild.isTextblock || - doc.firstChild.content.size !== 0 || - !this.placeholder - ) { - return null; - } - - return DecorationSet.create(doc, [ - Decoration.node(0, doc.firstChild.nodeSize, { - class: "placeholder", - "data-placeholder": this.placeholder, - }), - ]); - }, - }, - }); - } - - /** - * Creates a plugin that removes orphaned hard breaks from empty paragraphs. - * - * In XHTML contexts the trailing break element in paragraphs are rendered as - * uppercase (<BR> instead of <br>). ProseMirror seems to have issues parsing - * these breaks, which leads to orphaned breaks after deleting text content. - * - * @returns {PmPlugin} - */ - #createCleanupOrphanedBreaksPlugin() { - return new PmPlugin({ - appendTransaction(transactions, prevState, nextState) { - if (!transactions.some(tr => tr.docChanged)) { - return null; - } - - const tr = nextState.tr; - let modified = false; - - nextState.doc.descendants((nextNode, nextPos) => { - if ( - nextNode.type.name !== "paragraph" || - nextNode.textContent || - nextNode.childCount === 0 - ) { - return true; - } - - for (let i = 0; i < nextNode.childCount; i++) { - if (nextNode.child(i).type.name === "hard_break") { - const prevNode = prevState.doc.nodeAt(nextPos); - if (prevNode?.type.name === "paragraph" && prevNode.textContent) { - tr.replaceWith( - nextPos + 1, - nextPos + nextNode.content.size + 1, - [] - ); - modified = true; - } - break; - } - } - - return true; - }); - - return modified ? tr : null; - }, - }); - } - - #refreshView() { - if (!this.#view) { - return; - } - - this.#view.setProps({ - attributes: this.#viewAttributes(), - editable: () => !this.readOnly, - }); - this.#view.dispatch(this.#view.state.tr); - } - - #textOffsetFromPos(pos, doc = this.#view?.state.doc) { - if (!doc) { - return 0; - } - return doc.textBetween(0, pos, "\n", "\n").length; - } - - #posFromTextOffset(offset, doc = this.#view?.state.doc) { - if (!doc) { - return 0; - } - const target = Math.max(0, Math.min(offset ?? 0, this.#textLength(doc))); - let seen = 0; - let pos = doc.content.size; - let found = false; - let paragraphCount = 0; - doc.descendants((node, nodePos) => { - if (found) { - return false; - } - if (node.type.name === "paragraph") { - if (paragraphCount > 0) { - if (target <= seen + 1) { - pos = nodePos; - found = true; - return false; - } - seen += 1; - } - paragraphCount++; - } - if (node.isText) { - const textNodeLength = node.text.length; - const start = nodePos; - if (target <= seen + textNodeLength) { - pos = start + (target - seen); - found = true; - return false; - } - seen += textNodeLength; - } else if (node.type.name === "hard_break") { - if (target <= seen + 1) { - pos = nodePos; - found = true; - return false; - } - seen += 1; - } - return true; - }); - return pos; - } - - #textLength(doc) { - if (!doc) { - return 0; - } - return doc.textBetween(0, doc.content.size, "\n", "\n").length; - } - - #viewAttributes() { - return { - "aria-label": this.placeholder, - "aria-multiline": "true", - "aria-readonly": this.readOnly ? "true" : "false", - role: "textbox", - }; - } - - render() { - return html` - <link - rel="stylesheet" - href="chrome://browser/content/multilineeditor/prosemirror.css" - /> - <link - rel="stylesheet" - href="chrome://browser/content/multilineeditor/multiline-editor.css" - /> - <div class="multiline-editor"></div> - `; - } -} - -customElements.define("moz-multiline-editor", MultilineEditor); diff --git a/browser/components/multilineeditor/multiline-editor.stories.mjs b/browser/components/multilineeditor/multiline-editor.stories.mjs @@ -1,26 +0,0 @@ -/* 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/. */ - -import { html } from "chrome://global/content/vendor/lit.all.mjs"; -import "chrome://browser/content/multilineeditor/multiline-editor.mjs"; - -export default { - title: "UI Widgets/Multiline Editor", - component: "moz-multiline-editor", - argTypes: { - action: { - options: [null, "chat", "search", "navigate"], - control: { type: "select" }, - }, - }, -}; - -const Template = ({ placeholder }) => html` - <moz-multiline-editor .placeholder=${placeholder}></moz-multiline-editor> -`; - -export const Default = Template.bind({}); -Default.args = { - placeholder: "Placeholder text", -}; diff --git a/browser/components/multilineeditor/tests/chrome/chrome.toml b/browser/components/multilineeditor/tests/chrome/chrome.toml @@ -1,12 +0,0 @@ -[DEFAULT] -support-files = [ - "../../../../../toolkit/content/tests/widgets/lit-test-helpers.js", -] - -["test_multiline_editor_input.html"] - -["test_multiline_editor_rendering.html"] - -["test_multiline_editor_selection.html"] - -["test_multiline_editor_value.html"] diff --git a/browser/components/multilineeditor/tests/chrome/test_multiline_editor_input.html b/browser/components/multilineeditor/tests/chrome/test_multiline_editor_input.html @@ -1,90 +0,0 @@ -<!doctype html> -<html> - <head> - <meta charset="utf-8" /> - <title>Multiline editor input test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <link - rel="stylesheet" - href="chrome://mochikit/content/tests/SimpleTest/test.css" - /> - <link rel="stylesheet" href="chrome://global/skin/global.css" /> - <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> - <script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script> - <script - type="module" - src="chrome://browser/content/multilineeditor/multiline-editor.mjs" - ></script> - <script> - let html; - let testHelpers; - - add_setup(async function setup() { - testHelpers = new LitTestHelpers(); - ({ html } = await testHelpers.setupLit()); - testHelpers.setupTests({ - templateFn: (attributes) => html`<moz-multiline-editor ${attributes}></moz-multiline-editor>`, - }); - }); - - add_task(async function testInputEvent() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - let inputEventFired = false; - const inputPromise = BrowserTestUtils.waitForEvent(editor, "input"); - editor.addEventListener("input", () => { - inputEventFired = true; - }); - - editor.focus(); - sendString("a"); - await inputPromise; - - ok(inputEventFired, "Input event was fired"); - ok(editor.value.includes("a"), "Value updated after input"); - }); - - add_task(async function testFocus() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.focus(); - const shadowRoot = editor.shadowRoot; - const editorElement = shadowRoot.querySelector(".multiline-editor"); - ok(editorElement, "Editor element exists in shadow DOM"); - - ok(shadowRoot.activeElement, "Shadow DOM has an active element after focus"); - is(document.activeElement, editor, "Editor element is the active element in document"); - }); - - add_task(async function testReadOnlyPreventsUserInput() { - const result = await testHelpers.renderTemplate(html` - <moz-multiline-editor readonly></moz-multiline-editor> - `); - const editor = result.querySelector("moz-multiline-editor"); - - is(editor.readOnly, true, "Editor is read-only"); - is(editor.value, "", "Initial value is empty"); - - let inputEventFired = false; - editor.addEventListener("input", () => { - inputEventFired = true; - }); - - editor.focus(); - sendString("test"); - - await new Promise(resolve => requestAnimationFrame(resolve)); - - ok(!inputEventFired, "Input event not fired"); - is(editor.value, "", "Value remains empty"); - }); - </script> - </head> - <body> - <p id="display"></p> - <div id="content" style="display: none"></div> - <pre id="test"></pre> - </body> -</html> diff --git a/browser/components/multilineeditor/tests/chrome/test_multiline_editor_rendering.html b/browser/components/multilineeditor/tests/chrome/test_multiline_editor_rendering.html @@ -1,64 +0,0 @@ -<!doctype html> -<html> - <head> - <meta charset="utf-8" /> - <title>Multiline editor rendering test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <link - rel="stylesheet" - href="chrome://mochikit/content/tests/SimpleTest/test.css" - /> - <link rel="stylesheet" href="chrome://global/skin/global.css" /> - <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> - <script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script> - <script - type="module" - src="chrome://browser/content/multilineeditor/multiline-editor.mjs" - ></script> - <script> - let html; - let testHelpers; - - add_setup(async function setup() { - testHelpers = new LitTestHelpers(); - ({ html } = await testHelpers.setupLit()); - testHelpers.setupTests({ - templateFn: (attributes) => html`<moz-multiline-editor ${attributes}></moz-multiline-editor>`, - }); - }); - - add_task(async function testBasicRendering() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - ok(editor, "Editor element is rendered"); - is(editor.value, "", "Editor has empty value"); - is(editor.placeholder, "", "Editor has empty placeholder"); - is(editor.readOnly, false, "Editor is not read-only by default"); - }); - - add_task(async function testPlaceholder() { - const result = await testHelpers.renderTemplate(html` - <moz-multiline-editor placeholder="Enter text here"></moz-multiline-editor> - `); - const editor = result.querySelector("moz-multiline-editor"); - - is(editor.placeholder, "Enter text here", "Placeholder is set correctly"); - }); - - add_task(async function testReadOnly() { - const result = await testHelpers.renderTemplate(html` - <moz-multiline-editor readonly></moz-multiline-editor> - `); - const editor = result.querySelector("moz-multiline-editor"); - - is(editor.readOnly, true, "Editor is read-only"); - }); - </script> - </head> - <body> - <p id="display"></p> - <div id="content" style="display: none"></div> - <pre id="test"></pre> - </body> -</html> diff --git a/browser/components/multilineeditor/tests/chrome/test_multiline_editor_selection.html b/browser/components/multilineeditor/tests/chrome/test_multiline_editor_selection.html @@ -1,113 +0,0 @@ -<!doctype html> -<html> - <head> - <meta charset="utf-8" /> - <title>Multiline editor selection test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <link - rel="stylesheet" - href="chrome://mochikit/content/tests/SimpleTest/test.css" - /> - <link rel="stylesheet" href="chrome://global/skin/global.css" /> - <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> - <script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script> - <script - type="module" - src="chrome://browser/content/multilineeditor/multiline-editor.mjs" - ></script> - <script> - let html; - let testHelpers; - - add_setup(async function setup() { - testHelpers = new LitTestHelpers(); - ({ html } = await testHelpers.setupLit()); - testHelpers.setupTests({ - templateFn: (attributes) => html`<moz-multiline-editor ${attributes}></moz-multiline-editor>`, - }); - }); - - add_task(async function testSelectionProperties() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Hello World"; - is(editor.selectionStart, 11, "Selection starts at end of text after setting value"); - is(editor.selectionEnd, 11, "Selection ends at end of text after setting value"); - }); - - add_task(async function testSetSelectionRange() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Hello World"; - editor.setSelectionRange(0, 5); - is(editor.selectionStart, 0, "Selection start is 0"); - is(editor.selectionEnd, 5, "Selection end is 5"); - }); - - add_task(async function testSelectAll() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - const text = "Hello World"; - editor.value = text; - editor.select(); - is(editor.selectionStart, 0, "Selection starts at beginning"); - is(editor.selectionEnd, text.length, "Selection ends at text length"); - }); - - add_task(async function testSelectionWithMultiline() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Line 1\nLine 2"; - editor.setSelectionRange(0, 6); - is(editor.selectionStart, 0, "Selection start is correct"); - is(editor.selectionEnd, 6, "Selection end is correct"); - }); - - add_task(async function testSelectionStartProperty() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Hello World"; - const initialEnd = editor.selectionEnd; - editor.selectionStart = 6; - is(editor.selectionStart, 6, "Selection start is set via property"); - is(editor.selectionEnd, initialEnd, "Selection end remains unchanged"); - }); - - add_task(async function testSelectionEndProperty() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Hello World"; - editor.setSelectionRange(0, 5); - editor.selectionEnd = 11; - is(editor.selectionStart, 0, "Selection start remains at 0"); - is(editor.selectionEnd, 11, "Selection end is set via property"); - }); - - add_task(async function testSelectionchangeEvent() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - let selectionChangeEventFired = false; - editor.addEventListener("selectionchange", () => { - selectionChangeEventFired = true; - }); - - editor.value = "Hello World"; - editor.setSelectionRange(0, 5); - - ok(selectionChangeEventFired, "Selectionchange event was fired"); - }); - </script> - </head> - <body> - <p id="display"></p> - <div id="content" style="display: none"></div> - <pre id="test"></pre> - </body> -</html> diff --git a/browser/components/multilineeditor/tests/chrome/test_multiline_editor_value.html b/browser/components/multilineeditor/tests/chrome/test_multiline_editor_value.html @@ -1,90 +0,0 @@ -<!doctype html> -<html> - <head> - <meta charset="utf-8" /> - <title>Multiline editor value test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <link - rel="stylesheet" - href="chrome://mochikit/content/tests/SimpleTest/test.css" - /> - <link rel="stylesheet" href="chrome://global/skin/global.css" /> - <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> - <script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script> - <script - type="module" - src="chrome://browser/content/multilineeditor/multiline-editor.mjs" - ></script> - <script> - let html; - let testHelpers; - - add_setup(async function setup() { - testHelpers = new LitTestHelpers(); - ({ html } = await testHelpers.setupLit()); - testHelpers.setupTests({ - templateFn: (attributes) => html`<moz-multiline-editor ${attributes}></moz-multiline-editor>`, - }); - }); - - add_task(async function testSingleWordValue() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Hello"; - is(editor.value, "Hello", "Value with single word is set correctly"); - }); - - add_task(async function testMultiWordValue() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Hello and goodbye world"; - is(editor.value, "Hello and goodbye world", "Value with multiple words is set correctly"); - }); - - add_task(async function testMultilineValue() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - const text = "Line 1\nLine 2\nLine 3"; - editor.value = text; - is(editor.value, text, "Multiline text is set correctly"); - }); - - add_task(async function testClearValue() { - const result = await testHelpers.renderTemplate(); - const editor = result.querySelector("moz-multiline-editor"); - - editor.value = "Some text"; - is(editor.value, "Some text", "Value is set"); - - editor.value = ""; - is(editor.value, "", "Value is cleared"); - }); - - add_task(async function testPendingValue() { - const editor = document.createElement("moz-multiline-editor"); - - // Set value before rendering completed - editor.value = "Pending text"; - is(editor.value, "Pending text", "Value getter returns pending value before view creation"); - ok(!editor.shadowRoot, "Shadow root doesn’t exist before connection"); - - const result = await testHelpers.renderTemplate(); - result.appendChild(editor); - - // Wait for editor to complete rendering - await editor.updateComplete; - - ok(editor.shadowRoot, "Shadow root exists"); - is(editor.value, "Pending text", "Pending value is applied"); - }); - </script> - </head> - <body> - <p id="display"></p> - <div id="content" style="display: none"></div> - <pre id="test"></pre> - </body> -</html> diff --git a/browser/components/multilineeditor/tests/moz.build b/browser/components/multilineeditor/tests/moz.build @@ -1,8 +0,0 @@ -# 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/. - -with Files("**"): - BUG_COMPONENT = ("Firefox", "Address Bar") - -MOCHITEST_CHROME_MANIFESTS += ["chrome/chrome.toml"] diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js @@ -39,8 +39,6 @@ module.exports = { `${projectRoot}/browser/components/webrtc/content/**/*.stories.mjs`, // AI Window components stories `${projectRoot}/browser/components/aiwindow/ui/**/*.stories.mjs`, - // Multiline editor components stories - `${projectRoot}/browser/components/multilineeditor/**/*.stories.mjs`, // Everything else "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|md)", // Design system files @@ -90,7 +88,6 @@ module.exports = { // Make whatever fine-grained changes you need config.resolve.alias = { browser: `${projectRoot}/browser`, - third_party: `${projectRoot}/third_party`, toolkit: `${projectRoot}/toolkit`, "toolkit-widgets": `${projectRoot}/toolkit/content/widgets/`, "lit.all.mjs": `${projectRoot}/toolkit/content/widgets/vendor/lit.all.mjs`, diff --git a/browser/components/urlbar/content/SmartbarInput.mjs b/browser/components/urlbar/content/SmartbarInput.mjs @@ -1,6271 +0,0 @@ -/* 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/. */ - -import { createEditor } from "chrome://browser/content/urlbar/SmartbarInputUtils.mjs"; - -const { XPCOMUtils } = ChromeUtils.importESModule( - "resource://gre/modules/XPCOMUtils.sys.mjs" -); - -const { AppConstants } = ChromeUtils.importESModule( - "resource://gre/modules/AppConstants.sys.mjs" -); - -/** - * @import {UrlbarSearchOneOffs} from "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs" - */ - -const lazy = XPCOMUtils.declareLazy({ - ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", - BrowserSearchTelemetry: - "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", - BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", - BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", - ExtensionSearchHandler: - "resource://gre/modules/ExtensionSearchHandler.sys.mjs", - ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", - ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", - PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs", - PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", - ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs", - SearchModeSwitcher: - "moz-src:///browser/components/urlbar/SearchModeSwitcher.sys.mjs", - SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", - SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", - SmartbarInputController: - "chrome://browser/content/urlbar/SmartbarInputController.mjs", - UrlbarController: - "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs", - UrlbarEventBufferer: - "moz-src:///browser/components/urlbar/UrlbarEventBufferer.sys.mjs", - UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", - UrlbarQueryContext: - "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", - UrlbarProviderGlobalActions: - "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs", - UrlbarProviderOpenTabs: - "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs", - UrlbarSearchUtils: - "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", - UrlbarTokenizer: - "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", - UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", - UrlbarValueFormatter: - "moz-src:///browser/components/urlbar/UrlbarValueFormatter.sys.mjs", - UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs", - UrlbarSearchTermsPersistence: - "moz-src:///browser/components/urlbar/UrlbarSearchTermsPersistence.sys.mjs", - UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs", - ClipboardHelper: { - service: "@mozilla.org/widget/clipboardhelper;1", - iid: Ci.nsIClipboardHelper, - }, - QueryStringStripper: { - service: "@mozilla.org/url-query-string-stripper;1", - iid: Ci.nsIURLQueryStringStripper, - }, - QUERY_STRIPPING_STRIP_ON_SHARE: { - pref: "privacy.query_stripping.strip_on_share.enabled", - default: false, - }, - logger: () => lazy.UrlbarUtils.getLogger({ prefix: "SmartbarInput" }), -}); - -const UNLIMITED_MAX_RESULTS = 99; - -let getBoundsWithoutFlushing = element => - element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element); -let px = number => number.toFixed(2) + "px"; - -/** - * Implements the text input part of the address bar UI. - */ -export class SmartbarInput extends HTMLElement { - static get #markup() { - return ` - <hbox class="urlbar-background"/> - <hbox class="urlbar-input-container" - flex="1" - pageproxystate="invalid"> - <moz-urlbar-slot name="remote-control-box"> </moz-urlbar-slot> - <toolbarbutton id="urlbar-searchmode-switcher" - class="searchmode-switcher chromeclass-toolbar-additional" - align="center" - aria-expanded="false" - aria-haspopup="menu" - tooltip="dynamic-shortcut-tooltip" - data-l10n-id="urlbar-searchmode-default" - type="menu"> - <image class="searchmode-switcher-icon toolbarbutton-icon"/> - <image class="searchmode-switcher-dropmarker toolbarbutton-icon toolbarbutton-combined-buttons-dropmarker" - data-l10n-id="urlbar-searchmode-dropmarker" /> - <menupopup class="searchmode-switcher-popup toolbar-menupopup" - consumeoutsideclicks="false"> - <label class="searchmode-switcher-popup-description" - data-l10n-id="urlbar-searchmode-popup-description" - role="heading" /> - <menuseparator/> - <menuseparator class="searchmode-switcher-popup-footer-separator"/> - <menuitem class="searchmode-switcher-popup-search-settings-button menuitem-iconic" - data-action="openpreferences" - image="chrome://global/skin/icons/settings.svg" - data-l10n-id="urlbar-searchmode-popup-search-settings-menuitem"/> - </menupopup> - </toolbarbutton> - <box class="searchmode-switcher-chicklet"> - <label class="searchmode-switcher-title" /> - <toolbarbutton class="searchmode-switcher-close toolbarbutton-icon close-button" - data-action="exitsearchmode" - role="button" - data-l10n-id="urlbar-searchmode-exit-button" /> - </box> - <moz-urlbar-slot name="site-info"> </moz-urlbar-slot> - <moz-input-box tooltip="aHTMLTooltip" - class="urlbar-input-box" - flex="1" - role="combobox" - aria-owns="urlbar-results"> - <html:input id="urlbar-scheme" - required="required"/> - <html:input id="urlbar-input" - class="urlbar-input textbox-input" - aria-controls="urlbar-results" - aria-autocomplete="both" - inputmode="mozAwesomebar" - data-l10n-id="smartbar-placeholder"/> - </moz-input-box> - <moz-urlbar-slot name="revert-button"> </moz-urlbar-slot> - <image class="urlbar-icon urlbar-go-button" - role="button" - data-l10n-id="urlbar-go-button"/> - <moz-urlbar-slot name="page-actions" hidden=""> </moz-urlbar-slot> - </hbox> - <vbox class="urlbarView" - context="" - role="group" - tooltip="aHTMLTooltip"> - <html:div class="urlbarView-body-outer"> - <html:div class="urlbarView-body-inner"> - <html:div id="urlbar-results" - class="urlbarView-results" - role="listbox"/> - </html:div> - </html:div> - <menupopup class="urlbarView-result-menu" - consumeoutsideclicks="false"/> - <hbox class="search-one-offs" - includecurrentengine="true" - disabletab="true"/> - </vbox>`; - } - - /** - * @type {DocumentFragment=} - * - * The cached fragment. - */ - static #fragment; - - /** @type {DocumentFragment} */ - static get fragment() { - if (!this.#fragment) { - this.#fragment = window.MozXULElement.parseXULToFragment(this.#markup); - } - // @ts-ignore - return document.importNode(this.#fragment, true); - } - - static #inputFieldEvents = [ - "compositionstart", - "compositionend", - "contextmenu", - "dragover", - "dragstart", - "drop", - "focus", - "blur", - "input", - "beforeinput", - "keydown", - "keyup", - "mouseover", - "overflow", - "underflow", - "paste", - "scrollend", - "select", - "selectionchange", - ]; - - static #validSmartbarModes = ["chat", "search", "navigate"]; - - #allowBreakout = false; - #gBrowserListenersAdded = false; - #breakoutBlockerCount = 0; - #isAddressbar = false; - /** - * `True` if this instance is in `smartbar` mode. - * - * Smartbar mode is enabled by adding the attribute `sap-name="smartbar"`. - * Both `#isSmartbarMode` and `#isAddressbar` are `false` if `sap-name` is neither - * `smartbar` nor `urlbar`. - */ - #isSmartbarMode = false; - #sapName = ""; - #smartbarEditor = null; - #smartbarInputController = null; - #smartbarMode = "search"; - _userTypedValue = ""; - _actionOverrideKeyCount = 0; - _lastValidURLStr = ""; - _valueOnLastSearch = ""; - _suppressStartQuery = false; - _suppressPrimaryAdjustment = false; - _lastSearchString = ""; - // Tracks IME composition. - #compositionState = lazy.UrlbarUtils.COMPOSITION.NONE; - #compositionClosedPopup = false; - - valueIsTyped = false; - - // Properties accessed in tests. - lastQueryContextPromise = Promise.resolve(); - _autofillPlaceholder = null; - _resultForCurrentValue = null; - _untrimmedValue = ""; - _enableAutofillPlaceholder = true; - - constructor() { - super(); - - this.window = this.ownerGlobal; - this.document = this.window.document; - this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window); - - lazy.UrlbarPrefs.addObserver(this); - window.addEventListener("unload", () => { - // Stop listening to pref changes to make sure we don't init the new - // searchbar in closed windows that have not been gc'd yet. - lazy.UrlbarPrefs.removeObserver(this); - }); - } - - /** - * Populates moz-urlbar-slots by moving all children with a urlbar-slot - * attribute into their moz-urlbar-slots and removing the slots. - * - * Should only be called once all children have been parsed. - */ - #populateSlots() { - let urlbarSlots = this.querySelectorAll("moz-urlbar-slot[name]"); - for (let slot of urlbarSlots) { - let slotName = slot.getAttribute("name"); - let nodes = this.querySelectorAll(`:scope > [urlbar-slot="${slotName}"]`); - - for (let node of nodes) { - slot.parentNode.insertBefore(node, slot); - } - - slot.remove(); - } - - // Slotted elements only used by the addressbar. - // Will be null for searchbar and others. - this._identityBox = this.querySelector(".identity-box"); - this._revertButton = this.querySelector(".urlbar-revert-button"); - // Pre scotch bonnet search mode indicator (addressbar only). - this._searchModeIndicator = this.querySelector( - "#urlbar-search-mode-indicator" - ); - this._searchModeIndicatorTitle = this._searchModeIndicator?.querySelector( - "#urlbar-search-mode-indicator-title" - ); - this._searchModeIndicatorClose = this._searchModeIndicator?.querySelector( - "#urlbar-search-mode-indicator-close" - ); - } - - /** - * Initialization that happens once on the first connect. - */ - #initOnce() { - this.#sapName = this.getAttribute("sap-name"); - this.#isAddressbar = this.#sapName == "urlbar"; - this.#isSmartbarMode = this.#sapName == "smartbar"; - - // This listener must be added before connecting the fragment - // because the event could fire while or after connecting it. - this.addEventListener( - "moz-input-box-rebuilt", - this.#onContextMenuRebuilt.bind(this) - ); - - this.appendChild(SmartbarInput.fragment); - - // Make sure all children have been parsed before calling #populateSlots. - if (document.readyState === "loading") { - document.addEventListener( - "DOMContentLoaded", - () => this.#populateSlots(), - { once: true } - ); - } else { - this.#populateSlots(); - } - - this.panel = this.querySelector(".urlbarView"); - this.inputField = /** @type {HTMLInputElement} */ ( - this.querySelector(".urlbar-input") - ); - if (this.#isSmartbarMode) { - this.#ensureSmartbarEditor(); - } - this._inputContainer = this.querySelector(".urlbar-input-container"); - - this.controller = new lazy.UrlbarController({ input: this }); - this.view = new lazy.UrlbarView(this); - this.searchModeSwitcher = new lazy.SearchModeSwitcher(this); - - // The event bufferer can be used to defer events that may affect users - // muscle memory; for example quickly pressing DOWN+ENTER should end up - // on a predictable result, regardless of the search status. The event - // bufferer will invoke the handling code at the right time. - this.eventBufferer = new lazy.UrlbarEventBufferer(this); - - // Forward certain properties. - // Note if you are extending these, you'll also need to extend the inline - // type definitions. - const READ_WRITE_PROPERTIES = [ - "placeholder", - "readOnly", - "selectionStart", - "selectionEnd", - ]; - - for (let property of READ_WRITE_PROPERTIES) { - Object.defineProperty(this, property, { - enumerable: true, - get() { - return this.inputField[property]; - }, - set(val) { - this.inputField[property] = val; - }, - }); - } - - // The engine name is not known yet, but update placeholder anyway to - // reflect value of keyword.enabled or set the searchbar placeholder. - this._setPlaceholder(null); - - if (this.#isAddressbar) { - let searchContainersPref = lazy.UrlbarPrefs.get( - "switchTabs.searchAllContainers" - ); - Glean.urlbar.prefSwitchTabsSearchAllContainers.set(searchContainersPref); - } - } - - connectedCallback() { - if ( - this.getAttribute("sap-name") == "searchbar" && - !lazy.UrlbarPrefs.get("browser.search.widget.new") - ) { - return; - } - - this.#init(); - } - - #init() { - if (!this.controller) { - this.#initOnce(); - } - - if (this.sapName == "searchbar") { - this.parentNode.setAttribute("overflows", "false"); - } - - // Don't attach event listeners if the toolbar is not visible - // in this window or the urlbar is readonly. - if ( - !this.window.toolbar.visible || - this.window.document.documentElement.hasAttribute("taskbartab") || - this.readOnly - ) { - return; - } - - this._initCopyCutController(); - - for (let event of SmartbarInput.#inputFieldEvents) { - this.inputField.addEventListener(event, this); - } - - // These are on the window to detect focusing shortcuts like F6. - this.window.addEventListener("keydown", this); - this.window.addEventListener("keyup", this); - - this.window.addEventListener("mousedown", this); - if (AppConstants.platform == "win") { - this.window.addEventListener("draggableregionleftmousedown", this); - } - this.addEventListener("mousedown", this); - - // This listener handles clicks from our children too, included the search mode - // indicator close button. - this._inputContainer.addEventListener("click", this); - - // This is used to detect commands launched from the panel, to avoid - // recording abandonment events when the command causes a blur event. - this.view.panel.addEventListener("command", this, true); - - this.window.addEventListener("customizationstarting", this); - this.window.addEventListener("aftercustomization", this); - this.window.addEventListener("toolbarvisibilitychange", this); - let menuToolbar = this.window.document.getElementById("toolbar-menubar"); - if (menuToolbar) { - menuToolbar.addEventListener("DOMMenuBarInactive", this); - menuToolbar.addEventListener("DOMMenuBarActive", this); - } - - if (this.window.gBrowser) { - // On startup, this will be called again by browser-init.js - // once gBrowser has been initialized. - this.addGBrowserListeners(); - } - - // If the search service is not initialized yet, the placeholder - // and icon will be updated in delayedStartupInit. - if ( - Cu.isESModuleLoaded("resource://gre/modules/SearchService.sys.mjs") && - Services.search.isInitialized - ) { - this.searchModeSwitcher.updateSearchIcon(); - this._updatePlaceholderFromDefaultEngine(); - } - - // Expanding requires a parent toolbar, and us not being read-only. - this.#allowBreakout = !!this.closest("toolbar"); - if (this.#allowBreakout) { - // TODO(emilio): This could use CSS anchor positioning rather than this - // ResizeObserver, eventually. - this._resizeObserver = new this.window.ResizeObserver(([entry]) => { - this.style.setProperty( - "--urlbar-width", - px(entry.borderBoxSize[0].inlineSize) - ); - }); - this._resizeObserver.observe(this.parentNode); - } - - this.#updateLayoutBreakout(); - - this._addObservers(); - } - - disconnectedCallback() { - if ( - this.getAttribute("sap-name") == "searchbar" && - !lazy.UrlbarPrefs.get("browser.search.widget.new") - ) { - return; - } - - this.#uninit(); - } - - #uninit() { - if (this.sapName == "searchbar") { - this.parentNode.removeAttribute("overflows"); - } - - if (this._copyCutController) { - this.inputField.controllers.removeController(this._copyCutController); - delete this._copyCutController; - } - - for (let event of SmartbarInput.#inputFieldEvents) { - this.inputField.removeEventListener(event, this); - } - - // These are on the window to detect focusing shortcuts like F6. - this.window.removeEventListener("keydown", this); - this.window.removeEventListener("keyup", this); - - this.window.removeEventListener("mousedown", this); - if (AppConstants.platform == "win") { - this.window.removeEventListener("draggableregionleftmousedown", this); - } - this.removeEventListener("mousedown", this); - - // This listener handles clicks from our children too, included the search mode - // indicator close button. - this._inputContainer.removeEventListener("click", this); - - // This is used to detect commands launched from the panel, to avoid - // recording abandonment events when the command causes a blur event. - this.view.panel.removeEventListener("command", this, true); - - this.window.removeEventListener("customizationstarting", this); - this.window.removeEventListener("aftercustomization", this); - this.window.removeEventListener("toolbarvisibilitychange", this); - let menuToolbar = this.window.document.getElementById("toolbar-menubar"); - if (menuToolbar) { - menuToolbar.removeEventListener("DOMMenuBarInactive", this); - menuToolbar.removeEventListener("DOMMenuBarActive", this); - } - if (this.#gBrowserListenersAdded) { - this.window.gBrowser.tabContainer.removeEventListener("TabSelect", this); - this.window.gBrowser.tabContainer.removeEventListener("TabClose", this); - this.window.gBrowser.removeTabsProgressListener(this); - this.#gBrowserListenersAdded = false; - } - - this._resizeObserver?.disconnect(); - - this._removeObservers(); - } - - /** - * This method is used to attach new context menu options to the urlbar - * context menu, i.e. the context menu of the moz-input-box. - * It is called when the moz-input-box rebuilds its context menu. - * - * Note that it might be called before #init has finished. - */ - #onContextMenuRebuilt() { - this._initStripOnShare(); - this._initPasteAndGo(); - } - - addGBrowserListeners() { - // The following listeners are only used for the address bar. - if (!this.#isAddressbar) { - return; - } - if (this.window.gBrowser && !this.#gBrowserListenersAdded) { - this.window.gBrowser.tabContainer.addEventListener("TabSelect", this); - this.window.gBrowser.tabContainer.addEventListener("TabClose", this); - this.window.gBrowser.addTabsProgressListener(this); - this.#gBrowserListenersAdded = true; - } - } - - #initSmartbarEditor() { - const adapter = createEditor(this.inputField); - this.#smartbarInputController = new lazy.SmartbarInputController(adapter); - this.inputField = adapter.input; - this.#smartbarEditor = adapter.editor; - } - - #ensureSmartbarEditor() { - if (!this.#smartbarInputController) { - this.#initSmartbarEditor(); - } - return this.#smartbarEditor; - } - - #setInputValue(val) { - if (this.#smartbarInputController) { - this.#smartbarInputController.setValue(val); - } else { - this.inputField.value = val; - } - } - - #lazy = XPCOMUtils.declareLazy({ - valueFormatter: () => new lazy.UrlbarValueFormatter(this), - addSearchEngineHelper: () => new AddSearchEngineHelper(this), - }); - - /** - * Manages the Add Search Engine contextual menu entries. - */ - get addSearchEngineHelper() { - return this.#lazy.addSearchEngineHelper; - } - - /** - * The search access point name of the SmartbarInput for use with telemetry or - * logging, e.g. `urlbar`, `searchbar`. - */ - get sapName() { - return this.#sapName; - } - - get smartbarMode() { - const mode = this.getAttribute("smartbar-mode") || this.#smartbarMode; - return SmartbarInput.#validSmartbarModes.includes(mode) - ? mode - : SmartbarInput.#validSmartbarModes[0]; - } - - set smartbarMode(mode) { - if (!SmartbarInput.#validSmartbarModes.includes(mode)) { - return; - } - this.#smartbarMode = mode; - this.setAttribute("smartbar-mode", mode); - } - - blur() { - if (this.#smartbarInputController) { - this.#smartbarInputController.blur(); - } else { - this.inputField.blur(); - } - } - - /** - * @type {typeof HTMLInputElement.prototype.placeholder} - */ - get placeholder() { - return ( - this.#smartbarInputController?.placeholder ?? this.inputField?.placeholder - ); - } - - /** - * @type {typeof HTMLInputElement.prototype.placeholder} - */ - set placeholder(val) { - if (this.#smartbarInputController) { - this.#smartbarInputController.placeholder = val; - return; - } - if (this.inputField) { - this.inputField.placeholder = val; - } - } - - /** - * @type {typeof HTMLInputElement.prototype.readOnly} - */ - get readOnly() { - return this.#smartbarInputController?.readOnly ?? this.inputField?.readOnly; - } - - /** - * @type {typeof HTMLInputElement.prototype.readOnly} - */ - set readOnly(val) { - if (this.#smartbarInputController) { - this.#smartbarInputController.readOnly = val; - return; - } - if (this.inputField) { - this.inputField.readOnly = val; - } - } - - /** - * @type {typeof HTMLInputElement.prototype.selectionStart} - */ - get selectionStart() { - return ( - this.#smartbarInputController?.selectionStart ?? - this.inputField?.selectionStart ?? - 0 - ); - } - - /** - * @type {typeof HTMLInputElement.prototype.selectionStart} - */ - set selectionStart(val) { - if (this.#smartbarInputController) { - this.#smartbarInputController.selectionStart = val; - return; - } - if (this.inputField) { - this.inputField.selectionStart = val; - } - } - - /** - * @type {typeof HTMLInputElement.prototype.selectionEnd} - */ - get selectionEnd() { - return ( - this.#smartbarInputController?.selectionEnd ?? - this.inputField?.selectionEnd ?? - 0 - ); - } - - /** - * @type {typeof HTMLInputElement.prototype.selectionEnd} - */ - set selectionEnd(val) { - if (this.#smartbarInputController) { - this.#smartbarInputController.selectionEnd = val; - return; - } - if (this.inputField) { - this.inputField.selectionEnd = val; - } - } - - /** - * Called when a urlbar or urlbar related pref changes. - * - * @param {string} pref - * The name of the pref. Relative to `browser.urlbar` for urlbar prefs. - */ - onPrefChanged(pref) { - switch (pref) { - case "keyword.enabled": - this._updatePlaceholderFromDefaultEngine().catch(e => - // This can happen if the search service failed. - console.warn("Falied to update urlbar placeholder:", e) - ); - break; - case "browser.search.widget.new": { - if (this.getAttribute("sap-name") == "searchbar" && this.isConnected) { - if (lazy.UrlbarPrefs.get("browser.search.widget.new")) { - // The connectedCallback was skipped. Init now. - this.#init(); - } else { - // Uninit now, the disconnectedCallback will be skipped. - this.#uninit(); - } - } - } - } - } - - /** - * Applies styling to the text in the urlbar input, depending on the text. - */ - formatValue() { - // The editor may not exist if the toolbar is not visible. - if (this.#isAddressbar && this.editor) { - this.#lazy.valueFormatter.update(); - } - } - - focus() { - let beforeFocus = new CustomEvent("beforefocus", { - bubbles: true, - cancelable: true, - }); - this.inputField.dispatchEvent(beforeFocus); - if (beforeFocus.defaultPrevented) { - return; - } - - if (this.#smartbarInputController) { - this.#smartbarInputController.focus(); - } else { - this.inputField.focus(); - } - } - - select() { - let beforeSelect = new CustomEvent("beforeselect", { - bubbles: true, - cancelable: true, - }); - this.inputField.dispatchEvent(beforeSelect); - if (beforeSelect.defaultPrevented) { - return; - } - - // See _on_select(). HTMLInputElement.select() dispatches a "select" - // event but does not set the primary selection. - this._suppressPrimaryAdjustment = true; - if (this.#smartbarInputController) { - this.#smartbarInputController?.select(); - } else { - this.inputField.select(); - } - this._suppressPrimaryAdjustment = false; - } - - setSelectionRange(selectionStart, selectionEnd) { - let beforeSelect = new CustomEvent("beforeselect", { - bubbles: true, - cancelable: true, - }); - this.inputField.dispatchEvent(beforeSelect); - if (beforeSelect.defaultPrevented) { - return; - } - - // See _on_select(). HTMLInputElement.select() dispatches a "select" - // event but does not set the primary selection. - this._suppressPrimaryAdjustment = true; - if (this.#smartbarInputController) { - this.#smartbarInputController.setSelectionRange( - selectionStart, - selectionEnd - ); - } else { - this.inputField.setSelectionRange(selectionStart, selectionEnd); - } - this._suppressPrimaryAdjustment = false; - } - - saveSelectionStateForBrowser(browser) { - let state = this.getBrowserState(browser); - state.selection = { - // When the value is empty, we're either on a blank page, or the whole - // text has been edited away. In the latter case we'll restore value to - // the current URI, and we want to fully select it. - start: this.value ? this.selectionStart : 0, - end: this.value ? this.selectionEnd : Number.MAX_SAFE_INTEGER, - // When restoring a URI from an empty value, we don't want to untrim it. - shouldUntrim: this.value && !this._protocolIsTrimmed, - }; - } - - restoreSelectionStateForBrowser(browser) { - // Address bar must be focused to untrim and for selection to make sense. - this.focus(); - let state = this.getBrowserState(browser); - if (state.selection) { - if (state.selection.shouldUntrim) { - this.#maybeUntrimUrl(); - } - this.setSelectionRange( - state.selection.start, - // When selecting all the end value may be larger than the actual value. - Math.min(state.selection.end, this.value.length) - ); - } - } - - /** - * Sets the URI to display in the location bar. - * - * @param {object} [options] - * @param {?nsIURI} [options.uri] - * If this is unspecified, the current URI will be used. - * @param {boolean} [options.dueToTabSwitch=false] - * Whether this is being called due to switching tabs. - * @param {boolean} [options.dueToSessionRestore=false] - * Whether this is being called due to session restore. - * @param {boolean} [options.hideSearchTerms=false] - * True if userTypedValue should not be overidden by search terms - * and false otherwise. - * @param {boolean} [options.isSameDocument=false] - * Whether the caller loaded a new document or not (e.g. location - * change from an anchor scroll or a pushState event). - */ - setURI({ - uri = null, - dueToTabSwitch = false, - dueToSessionRestore = false, - hideSearchTerms = false, - isSameDocument = false, - } = {}) { - if (!this.#isAddressbar) { - throw new Error( - "Cannot set URI for SmartbarInput that is not an address bar" - ); - } - // We only need to update the searchModeUI on tab switch conditionally - // as we only persist searchMode with ScotchBonnet enabled. - if ( - dueToTabSwitch && - lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.persistSearchMode") - ) { - this._updateSearchModeUI(this.searchMode); - } - - let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); - this.#handlePersistedSearchTerms({ - state, - uri, - dueToTabSwitch, - hideSearchTerms, - isSameDocument, - }); - - let value = this.userTypedValue; - let valid = false; - let isReverting = !uri; - - // If `value` is null or if it's an empty string and we're switching tabs - // set value to the browser's current URI. When a user empties the input, - // switches tabs, and switches back, we want the URI to become visible again - // so the user knows what URI they're viewing. - // An exception to this is made in case of an auth request from a different - // base domain. To avoid auth prompt spoofing we already display the url of - // the cross domain resource, although the page is not loaded yet. - // This url will be set/unset by PromptParent. See bug 791594 for reference. - if (value === null || (!value && dueToTabSwitch)) { - uri = - this.window.gBrowser.selectedBrowser.currentAuthPromptURI || - uri || - this.#isOpenedPageInBlankTargetLoading || - this.window.gBrowser.currentURI; - // Strip off usernames and passwords for the location bar - try { - uri = Services.io.createExposableURI(uri); - } catch (e) {} - - let isInitialPageControlledByWebContent = false; - - // Replace initial page URIs with an empty string - // only if there's no opener (bug 370555). - if ( - this.window.isInitialPage(uri) && - lazy.BrowserUIUtils.checkEmptyPageOrigin( - this.window.gBrowser.selectedBrowser, - uri - ) - ) { - value = ""; - } else { - isInitialPageControlledByWebContent = true; - - // We should deal with losslessDecodeURI throwing for exotic URIs - try { - value = losslessDecodeURI(uri); - } catch (ex) { - value = "about:blank"; - } - } - // If we update the URI while restoring a session, set the proxyState to - // invalid, because we don't have a valid security state to show via site - // identity yet. See Bug 1746383. - valid = - !dueToSessionRestore && - (!this.window.isBlankPageURL(uri.spec) || - lazy.ExtensionUtils.isExtensionUrl(uri) || - isInitialPageControlledByWebContent); - } else if ( - this.window.isInitialPage(value) && - lazy.BrowserUIUtils.checkEmptyPageOrigin( - this.window.gBrowser.selectedBrowser - ) - ) { - value = ""; - valid = true; - } - - const previousUntrimmedValue = this.untrimmedValue; - // When calculating the selection indices we must take into account a - // trimmed protocol. - let offset = this._protocolIsTrimmed - ? lazy.BrowserUIUtils.trimURLProtocol.length - : 0; - const previousSelectionStart = this.selectionStart + offset; - const previousSelectionEnd = this.selectionEnd + offset; - - this._setValue(value, { allowTrim: true, valueIsTyped: !valid }); - this.toggleAttribute("usertyping", !valid && value); - - if (this.focused && value != previousUntrimmedValue) { - if ( - previousSelectionStart != previousSelectionEnd && - value.substring(previousSelectionStart, previousSelectionEnd) === - previousUntrimmedValue.substring( - previousSelectionStart, - previousSelectionEnd - ) - ) { - // If the same text is in the same place as the previously selected text, - // the selection is kept. - this.setSelectionRange( - previousSelectionStart - offset, - previousSelectionEnd - offset - ); - } else if ( - previousSelectionEnd && - (previousUntrimmedValue.length === previousSelectionEnd || - value.length <= previousSelectionEnd) - ) { - // If the previous end caret is not 0 and the caret is at the end of the - // input or its position is beyond the end of the new value, keep the - // position at the end. - this.setSelectionRange(value.length, value.length); - } else { - // Otherwise clear selection and set the caret position to the previous - // caret end position. - this.setSelectionRange( - previousSelectionEnd - offset, - previousSelectionEnd - offset - ); - } - } - - // The proxystate must be set before setting search mode below because - // search mode depends on it. - this.setPageProxyState( - valid ? "valid" : "invalid", - dueToTabSwitch, - !isReverting && - dueToTabSwitch && - this.getBrowserState(this.window.gBrowser.selectedBrowser) - .isUnifiedSearchButtonAvailable - ); - - if ( - state.persist?.shouldPersist && - !lazy.UrlbarSearchTermsPersistence.searchModeMatchesState( - this.searchMode, - state - ) - ) { - // When search terms persist, on non-default engine search result pages - // the address bar should show the same search mode. For default engines, - // search mode should not persist. - if (state.persist.isDefaultEngine) { - this.searchMode = null; - } else { - this.searchMode = { - engineName: state.persist.originalEngineName, - source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, - isPreview: false, - }; - } - } else if (dueToTabSwitch && !valid) { - // If we're switching tabs, restore the tab's search mode. - this.restoreSearchModeState(); - } else if (valid) { - // If the URI is valid, exit search mode. This must happen - // after setting proxystate above because search mode depends on it. - this.searchMode = null; - } - - // Dispatch URIUpdate event to synchronize the tab status when switching. - let event = new CustomEvent("SetURI", { bubbles: true }); - this.inputField.dispatchEvent(event); - } - - /** - * Converts an internal URI (e.g. a URI with a username or password) into one - * which we can expose to the user. - * - * @param {nsIURI} uri - * The URI to be converted - * @returns {nsIURI} - * The converted, exposable URI - */ - makeURIReadable(uri) { - // Avoid copying 'about:reader?url=', and always provide the original URI: - // Reader mode ensures we call createExposableURI itself. - let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay( - uri.displaySpec - ); - if (readerStrippedURI) { - return readerStrippedURI; - } - - try { - return Services.io.createExposableURI(uri); - } catch (ex) {} - - return uri; - } - - /** - * Function for tabs progress listener. - * - * @param {nsIBrowser} browser - * @param {nsIWebProgress} webProgress - * The nsIWebProgress instance that fired the notification. - * @param {nsIRequest} request - * The associated nsIRequest. This may be null in some cases. - * @param {nsIURI} locationURI - * The URI of the location that is being loaded. - */ - onLocationChange(browser, webProgress, request, locationURI) { - if (!webProgress.isTopLevel) { - return; - } - - if ( - browser != this.window.gBrowser.selectedBrowser && - !this.window.isBlankPageURL(locationURI.spec) - ) { - // If the page is loaded on background tab, make Unified Search Button - // unavailable when back to the tab. - this.getBrowserState(browser).isUnifiedSearchButtonAvailable = false; - } - - // Using browser navigation buttons should potentially trigger a bounce - // telemetry event. - if (webProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) { - this.controller.engagementEvent.handleBounceEventTrigger(browser); - } - } - - /** - * Passes DOM events to the _on_<event type> methods. - * - * @param {Event} event The event to handle. - */ - handleEvent(event) { - let methodName = "_on_" + event.type; - if (methodName in this) { - try { - this[methodName](event); - } catch (e) { - console.error(`Error calling SmartbarInput::${methodName}:`, e); - } - } else { - throw new Error("Unrecognized SmartbarInput event: " + event.type); - } - } - - /** - * Handles an event which might open text or a URL. If the event requires - * doing so, handleCommand forwards it to handleNavigation. - * - * @param {Event} [event] The event triggering the open. - */ - handleCommand(event = null) { - let isMouseEvent = MouseEvent.isInstance(event); - if (isMouseEvent && event.button == 2) { - // Do nothing for right clicks. - return; - } - - // Determine whether to use the selected one-off search button. In - // one-off search buttons parlance, "selected" means that the button - // has been navigated to via the keyboard. So we want to use it if - // the triggering event is not a mouse click -- i.e., it's a Return - // key -- or if the one-off was mouse-clicked. - if (this.view.isOpen) { - let selectedOneOff = this.view.oneOffSearchButtons?.selectedButton; - if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) { - this.view.oneOffSearchButtons.handleSearchCommand(event, { - engineName: selectedOneOff.engine?.name, - source: selectedOneOff.source, - entry: "oneoff", - }); - return; - } - } - - this.handleNavigation({ event }); - } - - /** - * @typedef {object} HandleNavigationOneOffParams - * - * @property {string} openWhere - * Where we expect the result to be opened. - * @property {object} openParams - * The parameters related to where the result will be opened. - * @property {nsISearchEngine} engine - * The selected one-off's engine. - */ - - /** - * Handles an event which would cause a URL or text to be opened. - * - * @param {object} options - * Options for the navigation. - * @param {Event} [options.event] - * The event triggering the open. - * @param {HandleNavigationOneOffParams} [options.oneOffParams] - * Optional. Pass if this navigation was triggered by a one-off. Practically - * speaking, UrlbarSearchOneOffs passes this when the user holds certain key - * modifiers while picking a one-off. In those cases, we do an immediate - * search using the one-off's engine instead of entering search mode. - * @param {object} [options.triggeringPrincipal] - * The principal that the action was triggered from. - */ - handleNavigation({ event, oneOffParams, triggeringPrincipal }) { - if (this.#isSmartbarMode) { - const committedValue = this.untrimmedValue; - const mode = this.smartbarMode; - lazy.logger.debug(`commit (${mode}): ${committedValue}`); - this.#clearSmartbarInput(); - this.dispatchEvent( - new CustomEvent("smartbar-commit", { - detail: { value: committedValue, event, mode }, - }) - ); - if (!this.window.gBrowser) { - return; - } - // Fall through to default navigation behaviour. - } - let element = this.view.selectedElement; - let result = this.view.getResultFromElement(element); - let openParams = oneOffParams?.openParams || { triggeringPrincipal }; - - // If the value was submitted during composition, the result may not have - // been updated yet, because the input event happens after composition end. - // We can't trust element nor _resultForCurrentValue targets in that case, - // so we always generate a new heuristic to load. - let isComposing = this.editor.composing; - - // Use the selected element if we have one; this is usually the case - // when the view is open. - let selectedPrivateResult = - result && - result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH && - result.payload.inPrivateWindow; - let selectedPrivateEngineResult = - selectedPrivateResult && result.payload.isPrivateEngine; - // Whether the user has been editing the value in the URL bar after selecting - // the result. However, if the result type is tip, pick as it is. The result - // heuristic is also kept the behavior as is for safety. - let safeToPickResult = - result && - (result.heuristic || - !this.valueIsTyped || - result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP || - this.value == this.#getValueFromResult(result)); - if ( - !isComposing && - element && - (!oneOffParams?.engine || selectedPrivateEngineResult) && - safeToPickResult - ) { - this.pickElement(element, event); - return; - } - - // Use the hidden heuristic if it exists and there's no selection. - if ( - lazy.UrlbarPrefs.get("experimental.hideHeuristic") && - !element && - !isComposing && - !oneOffParams?.engine && - this._resultForCurrentValue?.heuristic - ) { - this.pickResult(this._resultForCurrentValue, event); - return; - } - - // We don't select a heuristic result when we're autofilling a token alias, - // but we want pressing Enter to behave like the first result was selected. - if (!result && this.value.startsWith("@")) { - let tokenAliasResult = this.view.getResultAtIndex(0); - if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) { - this.pickResult(tokenAliasResult, event); - return; - } - } - - let url; - let selType = this.controller.engagementEvent.typeFromElement( - result, - element - ); - let typedValue = this.value; - if (oneOffParams?.engine) { - selType = "oneoff"; - typedValue = this._lastSearchString; - // If there's a selected one-off button then load a search using - // the button's engine. - result = this._resultForCurrentValue; - - let searchString = - (result && (result.payload.suggestion || result.payload.query)) || - this._lastSearchString; - [url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl( - oneOffParams.engine, - searchString - ); - if (oneOffParams.openWhere == "tab") { - this.window.gBrowser.tabContainer.addEventListener( - "TabOpen", - tabEvent => - this._recordSearch( - oneOffParams.engine, - event, - {}, - tabEvent.target.linkedBrowser - ), - { once: true } - ); - } else { - this._recordSearch(oneOffParams.engine, event); - } - - lazy.UrlbarUtils.addToFormHistory( - this, - searchString, - oneOffParams.engine.name - ).catch(console.error); - } else { - // Use the current value if we don't have a UrlbarResult e.g. because the - // view is closed. - url = this.untrimmedValue; - openParams.postData = null; - } - - if (!url) { - return; - } - - // When the user hits enter in a local search mode and there's no selected - // result or one-off, don't do anything. - if ( - this.searchMode && - !this.searchMode.engineName && - !result && - !oneOffParams - ) { - return; - } - - let where = oneOffParams?.openWhere || this._whereToOpen(event); - if (selectedPrivateResult) { - where = "window"; - openParams.private = true; - } - openParams.allowInheritPrincipal = false; - url = this._maybeCanonizeURL(event, url) || url.trim(); - - let selectedResult = result || this.view.selectedResult; - this.controller.engagementEvent.record(event, { - element, - selType, - searchString: typedValue, - result: selectedResult || this._resultForCurrentValue || null, - }); - - if (URL.canParse(url)) { - // Annotate if the untrimmed value contained a scheme, to later potentially - // be upgraded by schemeless HTTPS-First. - openParams.schemelessInput = this.#getSchemelessInput( - this.untrimmedValue - ); - this._loadURL(url, event, where, openParams); - return; - } - - // This is not a URL and there's no selected element, because likely the - // view is closed, or paste&go was used. - // We must act consistently here, having or not an open view should not - // make a difference if the search string is the same. - - // If we have a result for the current value, we can just use it. - if (!isComposing && this._resultForCurrentValue) { - this.pickResult(this._resultForCurrentValue, event); - return; - } - - // Otherwise, we must fetch the heuristic result for the current value. - // TODO (Bug 1604927): If the urlbar results are restricted to a specific - // engine, here we must search with that specific engine; indeed the - // docshell wouldn't know about our engine restriction. - // Also remember to invoke this._recordSearch, after replacing url with - // the appropriate engine submission url. - let browser = this.window.gBrowser.selectedBrowser; - let lastLocationChange = browser.lastLocationChange; - - // Increment rate denominator measuring how often Address Bar handleCommand fallback path is hit. - Glean.urlbar.heuristicResultMissing.addToDenominator(1); - - lazy.UrlbarUtils.getHeuristicResultFor(url, this) - .then(newResult => { - // Because this happens asynchronously, we must verify that the browser - // location did not change in the meanwhile. - if ( - where != "current" || - browser.lastLocationChange == lastLocationChange - ) { - this.pickResult(newResult, event, null, browser); - } - }) - .catch(() => { - if (url) { - // Something went wrong, we should always have a heuristic result, - // otherwise it means we're not able to search at all, maybe because - // some parts of the profile are corrupt. - // The urlbar should still allow to search or visit the typed string, - // so that the user can look for help to resolve the problem. - - // Increment rate numerator measuring how often Address Bar handleCommand fallback path is hit. - Glean.urlbar.heuristicResultMissing.addToNumerator(1); - - let flags = - Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | - Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; - if (this.isPrivate) { - flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; - } - let { - preferredURI: uri, - postData, - keywordAsSent, - } = Services.uriFixup.getFixupURIInfo(url, flags); - if ( - where != "current" || - browser.lastLocationChange == lastLocationChange - ) { - openParams.postData = postData; - if (!keywordAsSent) { - // `uri` is not a search engine url, so we annotate if the untrimmed - // value contained a scheme, to potentially be later upgraded by - // schemeless HTTPS-First. - openParams.schemelessInput = this.#getSchemelessInput( - this.untrimmedValue - ); - } - this._loadURL(uri.spec, event, where, openParams, null, browser); - } - } - }); - // Don't add further handling here, the catch above is our last resort. - } - - handleRevert() { - this.userTypedValue = null; - // Nullify search mode before setURI so it won't try to restore it. - this.searchMode = null; - if (this.#isAddressbar) { - this.setURI({ - dueToTabSwitch: true, - hideSearchTerms: true, - }); - } else { - this.value = ""; - } - if (this.value && this.focused) { - this.select(); - } - } - - maybeHandleRevertFromPopup(anchorElement) { - if (!this.#isAddressbar) { - return; - } - let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); - if (anchorElement?.closest("#urlbar") && state.persist?.shouldPersist) { - this.handleRevert(); - Glean.urlbarPersistedsearchterms.revertByPopupCount.add(1); - } - } - - /** - * Called by inputs that resemble search boxes, but actually hand input off - * to the Urlbar. We use these fake inputs on the new tab page and - * about:privatebrowsing. - * - * @param {string} searchString - * The search string to use. - * @param {nsISearchEngine} [searchEngine] - * Optional. If included and the right prefs are set, we will enter search - * mode when handing `searchString` from the fake input to the Urlbar. - * @param {string} [newtabSessionId] - * Optional. The id of the newtab session that handed off this search. - */ - handoff(searchString, searchEngine, newtabSessionId) { - this._isHandoffSession = true; - this._handoffSession = newtabSessionId; - if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) { - this.search(searchString, { - searchEngine, - searchModeEntry: "handoff", - }); - } else { - this.search(searchString); - } - } - - /** - * Called when an element of the view is picked. - * - * @param {HTMLElement} element The element that was picked. - * @param {Event} event The event that picked the element. - */ - pickElement(element, event) { - let result = this.view.getResultFromElement(element); - lazy.logger.debug( - `pickElement ${element} with event ${event?.type}, result: ${result}` - ); - if (!result) { - return; - } - this.pickResult(result, event, element); - } - - /** - * Called when a result is picked. - * - * @param {UrlbarResult} result The result that was picked. - * @param {Event} event The event that picked the result. - * @param {HTMLElement} element the picked view element, if available. - * @param {object} browser The browser to use for the load. - */ - // eslint-disable-next-line complexity - pickResult( - result, - event, - element = null, - browser = this.window.gBrowser.selectedBrowser - ) { - if (element?.classList.contains("urlbarView-button-menu")) { - this.view.openResultMenu(result, element); - return; - } - - if (element?.dataset.command) { - this.#pickMenuResult(result, event, element, browser); - return; - } - - if ( - result.providerName == lazy.UrlbarProviderGlobalActions.name && - this.#providesSearchMode(result) - ) { - this.maybeConfirmSearchModeFromResult({ - result, - checkValue: false, - }); - return; - } - - // When a one-off is selected, we restyle heuristic results to look like - // search results. In the unlikely event that they are clicked, instead of - // picking the results as usual, we confirm search mode, same as if the user - // had selected them and pressed the enter key. Restyling results in this - // manner was agreed on as a compromise between consistent UX and - // engineering effort. See review discussion at bug 1667766. - if ( - (this.searchMode?.isPreview && - result.providerName == lazy.UrlbarProviderGlobalActions.name) || - (result.heuristic && - this.searchMode?.isPreview && - this.view.oneOffSearchButtons?.selectedButton) - ) { - this.confirmSearchMode(); - this.search(this.value); - return; - } - - if ( - result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP && - result.payload.type == "dismissalAcknowledgment" - ) { - // The user clicked the "Got it" button inside the dismissal - // acknowledgment tip. Dismiss the tip. - this.controller.engagementEvent.record(event, { - result, - element, - searchString: this._lastSearchString, - selType: "dismiss", - }); - this.view.onQueryResultRemoved(result.rowIndex); - return; - } - - let resultUrl = element?.dataset.url; - let originalUntrimmedValue = this.untrimmedValue; - let isCanonized = this.setValueFromResult({ - result, - event, - element, - urlOverride: resultUrl, - }); - let where = this._whereToOpen(event); - let openParams = { - allowInheritPrincipal: false, - globalHistoryOptions: { - triggeringSource: this.#sapName, - triggeringSearchEngine: result.payload?.engine, - triggeringSponsoredURL: result.payload?.isSponsored - ? result.payload.url - : undefined, - }, - private: this.isPrivate, - }; - - if (resultUrl && where == "current") { - // Open help links in a new tab. - where = "tab"; - } - - if (!this.#providesSearchMode(result)) { - this.view.close({ elementPicked: true }); - } - - if (isCanonized) { - this.controller.engagementEvent.record(event, { - result, - element, - selType: "canonized", - searchString: this._lastSearchString, - }); - this._loadURL(this._untrimmedValue, event, where, openParams, browser); - return; - } - - let { url, postData } = resultUrl - ? { url: resultUrl, postData: null } - : lazy.UrlbarUtils.getUrlFromResult(result, { element }); - openParams.postData = postData; - - switch (result.type) { - case lazy.UrlbarUtils.RESULT_TYPE.URL: { - if (result.heuristic) { - // Bug 1578856: both the provider and the docshell run heuristics to - // decide how to handle a non-url string, either fixing it to a url, or - // searching for it. - // Some preferences can control the docshell behavior, for example - // if dns_first_for_single_words is true, the docshell looks up the word - // against the dns server, and either loads it as an url or searches for - // it, depending on the lookup result. The provider instead will always - // return a fixed url in this case, because URIFixup is synchronous and - // can't do a synchronous dns lookup. A possible long term solution - // would involve sharing the docshell logic with the provider, along - // with the dns lookup. - // For now, in this specific case, we'll override the result's url - // with the input value, and let it pass through to _loadURL(), and - // finally to the docshell. - // This also means that in some cases the heuristic result will show a - // Visit entry, but the docshell will instead execute a search. It's a - // rare case anyway, most likely to happen for enterprises customizing - // the urifixup prefs. - if ( - lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && - lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue) - ) { - url = originalUntrimmedValue; - } - // Annotate if the untrimmed value contained a scheme, to later potentially - // be upgraded by schemeless HTTPS-First. - openParams.schemelessInput = this.#getSchemelessInput( - originalUntrimmedValue - ); - } - break; - } - case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: { - // If this result comes from a bookmark keyword, let it inherit the - // current document's principal, otherwise bookmarklets would break. - openParams.allowInheritPrincipal = true; - break; - } - case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: { - // Behaviour is reversed with SecondaryActions, default behaviour is to navigate - // and button is provided to switch to tab. - if ( - this.hasAttribute("action-override") || - (lazy.UrlbarPrefs.get("secondaryActions.switchToTab") && - element?.dataset.action !== "tabswitch") - ) { - where = "current"; - break; - } - - // Keep the searchMode for telemetry since handleRevert sets it to null. - const searchMode = this.searchMode; - this.handleRevert(); - let prevTab = this.window.gBrowser.selectedTab; - let loadOpts = { - adoptIntoActiveWindow: lazy.UrlbarPrefs.get( - "switchTabs.adoptIntoActiveWindow" - ), - }; - - // We cache the search string because switching tab may clear it. - let searchString = this._lastSearchString; - this.controller.engagementEvent.record(event, { - result, - element, - searchString, - searchMode, - selType: this.controller.engagementEvent.typeFromElement( - result, - element - ), - }); - - let switched = this.window.switchToTabHavingURI( - Services.io.newURI(url), - true, - loadOpts, - lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") && - lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId( - result.payload.userContextId - ) - ? result.payload.userContextId - : null - ); - if (switched && prevTab.isEmpty) { - this.window.gBrowser.removeTab(prevTab); - } - - if (switched && !this.isPrivate && !result.heuristic) { - // We don't await for this, because a rejection should not interrupt - // the load. Just reportError it. - lazy.UrlbarUtils.addToInputHistory(url, searchString).catch( - console.error - ); - } - - // TODO (Bug 1865757): We should not show a "switchtotab" result for - // tabs that are not currently open. Find out why tabs are not being - // properly unregistered when they are being closed. - if (!switched) { - console.error(`Tried to switch to non-existent tab: ${url}`); - lazy.UrlbarProviderOpenTabs.unregisterOpenTab( - url, - result.payload.userContextId, - result.payload.tabGroup, - this.isPrivate - ); - } - - return; - } - case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: { - if (result.payload.providesSearchMode) { - this.controller.engagementEvent.record(event, { - result, - element, - searchString: this._lastSearchString, - selType: this.controller.engagementEvent.typeFromElement( - result, - element - ), - }); - this.maybeConfirmSearchModeFromResult({ - result, - checkValue: false, - }); - return; - } - - if ( - !this.searchMode && - result.heuristic && - // If we asked the DNS earlier, avoid the post-facto check. - !lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && - // TODO (bug 1642623): for now there is no smart heuristic to skip the - // DNS lookup, so any value above 0 will run it. - lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 && - this.window.gKeywordURIFixup && - lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue) - ) { - // When fixing a single word to a search, the docShell would also - // query the DNS and if resolved ask the user whether they would - // rather visit that as a host. On a positive answer, it adds the host - // to the list that we use to make decisions. - // Because we are directly asking for a search here, bypassing the - // docShell, we need to do the same ourselves. - // See also URIFixupChild.sys.mjs and keyword-uri-fixup. - let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim()); - if (fixupInfo) { - this.window.gKeywordURIFixup.check( - this.window.gBrowser.selectedBrowser, - fixupInfo - ); - } - } - - if (result.payload.inPrivateWindow) { - where = "window"; - openParams.private = true; - } - - const actionDetails = { - isSuggestion: !!result.payload.suggestion, - isFormHistory: - result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY, - alias: result.payload.keyword, - }; - const engine = Services.search.getEngineByName(result.payload.engine); - - if (where == "tab") { - // The TabOpen event is fired synchronously so tabEvent.target - // is guaranteed to be our new search tab. - this.window.gBrowser.tabContainer.addEventListener( - "TabOpen", - tabEvent => - this._recordSearch( - engine, - event, - actionDetails, - tabEvent.target.linkedBrowser - ), - { once: true } - ); - } else { - this._recordSearch(engine, event, actionDetails); - } - - if (!result.payload.inPrivateWindow) { - lazy.UrlbarUtils.addToFormHistory( - this, - result.payload.suggestion || result.payload.query, - engine.name - ).catch(console.error); - } - break; - } - case lazy.UrlbarUtils.RESULT_TYPE.TIP: { - if (url) { - break; - } - this.handleRevert(); - this.controller.engagementEvent.record(event, { - result, - element, - selType: "tip", - searchString: this._lastSearchString, - }); - return; - } - case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: { - if (!url) { - // If we're not loading a URL, the engagement is done. First revert - // and then record the engagement since providers expect the urlbar to - // be reverted when they're notified of the engagement, but before - // reverting, copy the search mode since it's nulled on revert. - const { searchMode } = this; - this.handleRevert(); - this.controller.engagementEvent.record(event, { - result, - element, - searchMode, - searchString: this._lastSearchString, - selType: this.controller.engagementEvent.typeFromElement( - result, - element - ), - }); - return; - } - break; - } - case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: { - this.controller.engagementEvent.record(event, { - result, - element, - selType: "extension", - searchString: this._lastSearchString, - }); - - // The urlbar needs to revert to the loaded url when a command is - // handled by the extension. - this.handleRevert(); - // We don't directly handle a load when an Omnibox API result is picked, - // instead we forward the request to the WebExtension itself, because - // the value may not even be a url. - // We pass the keyword and content, that actually is the retrieved value - // prefixed by the keyword. ExtensionSearchHandler uses this keyword - // redundancy as a sanity check. - lazy.ExtensionSearchHandler.handleInputEntered( - result.payload.keyword, - result.payload.content, - where - ); - return; - } - case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: { - this.handleRevert(); - this.controller.engagementEvent.record(event, { - result, - element, - searchString: this._lastSearchString, - selType: this.controller.engagementEvent.typeFromElement( - result, - element - ), - }); - this.maybeConfirmSearchModeFromResult({ - result, - checkValue: false, - }); - - return; - } - } - - if (!url) { - throw new Error(`Invalid url for result ${JSON.stringify(result)}`); - } - - // Record input history but only in non-private windows. - if (!this.isPrivate) { - let input; - if (!result.heuristic) { - input = this._lastSearchString; - } else if (result.autofill?.type == "adaptive") { - input = result.autofill.adaptiveHistoryInput; - } - // `input` may be an empty string, so do a strict comparison here. - if (input !== undefined) { - // We don't await for this, because a rejection should not interrupt - // the load. Just reportError it. - lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error); - } - } - - this.controller.engagementEvent.startTrackingBounceEvent(browser, event, { - result, - element, - searchString: this._lastSearchString, - selType: this.controller.engagementEvent.typeFromElement(result, element), - searchSource: this.getSearchSource(event), - }); - - this.controller.engagementEvent.record(event, { - result, - element, - searchString: this._lastSearchString, - selType: this.controller.engagementEvent.typeFromElement(result, element), - searchSource: this.getSearchSource(event), - }); - - if (result.payload.sendAttributionRequest) { - lazy.PartnerLinkAttribution.makeRequest({ - targetURL: result.payload.url, - source: this.#sapName, - campaignID: Services.prefs.getStringPref( - "browser.partnerlink.campaign.topsites" - ), - }); - if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") { - // The position is 1-based for telemetry - const position = result.rowIndex + 1; - Glean.contextualServicesTopsites.click[`urlbar_${position}`].add(1); - } - } - - this._loadURL( - url, - event, - where, - openParams, - { - source: result.source, - type: result.type, - searchTerm: result.payload.suggestion ?? result.payload.query, - }, - browser - ); - } - - #clearSmartbarInput() { - this.value = ""; - this.userTypedValue = ""; - this._lastSearchString = ""; - this._autofillPlaceholder = null; - this._resultForCurrentValue = null; - this.setSelectionRange(0, 0); - } - - /** - * Called by the view when moving through results with the keyboard, and when - * picking a result. This sets the input value to the value of the result and - * invalidates the pageproxystate. It also sets the result that is associated - * with the current input value. If you need to set this result but don't - * want to also set the input value, then use setResultForCurrentValue. - * - * @param {object} options - * Options. - * @param {UrlbarResult} [options.result] - * The result that was selected or picked, null if no result was selected. - * @param {Event} [options.event] - * The event that picked the result. - * @param {string} [options.urlOverride] - * Normally the URL is taken from `result.payload.url`, but if `urlOverride` - * is specified, it's used instead. See `#getValueFromResult()`. - * @param {Element} [options.element] - * The element that was selected or picked, if available. For results that - * have multiple selectable children, the value may be taken from a child - * element rather than the result. See `#getValueFromResult()`. - * @returns {boolean} - * Whether the value has been canonized - */ - setValueFromResult({ - result = null, - event = null, - urlOverride = null, - element = null, - } = {}) { - // Usually this is set by a previous input event, but in certain cases, like - // when opening Top Sites on a loaded page, it wouldn't happen. To avoid - // confusing the user, we always enforce it when a result changes our value. - this.setPageProxyState("invalid", true); - - // A previous result may have previewed search mode. If we don't expect that - // we might stay in a search mode of some kind, exit it now. - if ( - this.searchMode?.isPreview && - !this.#providesSearchMode(result) && - !this.view.oneOffSearchButtons?.selectedButton - ) { - this.searchMode = null; - } - - if (!result) { - // This happens when there's no selection, for example when moving to the - // one-offs search settings button, or to the input field when Top Sites - // are shown; then we must reset the input value. - // Note that for Top Sites the last search string would be empty, thus we - // must restore the last text value. - // Note that unselected autofill results will still arrive in this - // function with a non-null `result`. They are handled below. - this.value = this._lastSearchString || this._valueOnLastSearch; - this.setResultForCurrentValue(result); - return false; - } - - // We won't allow trimming when calling _setValue, since it makes too easy - // for the user to wrongly transform `https` into `http`, for example by - // picking a https://site/path_1 result and editing the path to path_2, - // then we'd end up visiting http://site/path_2. - // Trimming `http` would be ok, but there's other cases where it's unsafe, - // like transforming a url into a search. - // This choice also makes it easier to copy the full url of a result. - - // We are supporting canonization of any result, in particular this allows - // for single word search suggestions to be converted to a .com URL. - // For autofilled results, the value to canonize is the user typed string, - // not the autofilled value. - let canonizedUrl = this._maybeCanonizeURL( - event, - result.autofill ? this._lastSearchString : this.value - ); - if (canonizedUrl) { - this._setValue(canonizedUrl); - - this.setResultForCurrentValue(result); - return true; - } - - if (result.autofill) { - this._autofillValue(result.autofill); - } - - if (this.#providesSearchMode(result)) { - let enteredSearchMode; - // Only preview search mode if the result is selected. - if (this.view.resultIsSelected(result)) { - // For ScotchBonnet, As Tab and Arrow Down/Up, Page Down/Up key are used - // for selection of the urlbar results, keep the search mode as preview - // mode if there are multiple results. - // If ScotchBonnet is disabled, not starting a query means we will only - // preview search mode. - enteredSearchMode = this.maybeConfirmSearchModeFromResult({ - result, - checkValue: false, - startQuery: - lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") && - this.view.visibleResults.length == 1, - }); - } - if (!enteredSearchMode) { - this._setValue(this.#getValueFromResult(result), { - actionType: this.#getActionTypeFromResult(result), - }); - this.searchMode = null; - } - this.setResultForCurrentValue(result); - return false; - } - - if (!result.autofill) { - let value = this.#getValueFromResult(result, { urlOverride, element }); - this._setValue(value, { - actionType: this.#getActionTypeFromResult(result), - }); - } - - this.setResultForCurrentValue(result); - - // Update placeholder selection and value to the current selected result to - // prevent the on_selectionchange event to detect a "accent-character" - // insertion. - if (!result.autofill && this._autofillPlaceholder) { - this._autofillPlaceholder.value = this.value; - this._autofillPlaceholder.selectionStart = this.value.length; - this._autofillPlaceholder.selectionEnd = this.value.length; - } - return false; - } - - /** - * The input keeps track of the result associated with the current input - * value. This result can be set by calling either setValueFromResult or this - * method. Use this method when you need to set the result without also - * setting the input value. This can be the case when either the selection is - * cleared and no other result becomes selected, or when the result is the - * heuristic and we don't want to modify the value the user is typing. - * - * @param {UrlbarResult} result - * The result to associate with the current input value. - */ - setResultForCurrentValue(result) { - this._resultForCurrentValue = result; - } - - /** - * Called by the controller when the first result of a new search is received. - * If it's an autofill result, then it may need to be autofilled, subject to a - * few restrictions. - * - * @param {UrlbarResult} result - * The first result. - */ - _autofillFirstResult(result) { - if (!result.autofill) { - return; - } - - let isPlaceholderSelected = - this._autofillPlaceholder && - this.selectionEnd == this._autofillPlaceholder.value.length && - this.selectionStart == this._lastSearchString.length && - this._autofillPlaceholder.value - .toLocaleLowerCase() - .startsWith(this._lastSearchString.toLocaleLowerCase()); - - // Don't autofill if there's already a selection (with one caveat described - // next) or the cursor isn't at the end of the input. But if there is a - // selection and it's the autofill placeholder value, then do autofill. - if ( - !isPlaceholderSelected && - !this._autofillIgnoresSelection && - (this.selectionStart != this.selectionEnd || - this.selectionEnd != this._lastSearchString.length) - ) { - return; - } - - this.setValueFromResult({ result }); - } - /** - * Clears displayed autofill values and unsets the autofill placeholder. - */ - #clearAutofill() { - if (!this._autofillPlaceholder) { - return; - } - let currentSelectionStart = this.selectionStart; - let currentSelectionEnd = this.selectionEnd; - - // Overriding this value clears the selection. - this.#setInputValue( - this.value.substring(0, this._autofillPlaceholder.selectionStart) - ); - this._autofillPlaceholder = null; - // Restore selection - this.setSelectionRange(currentSelectionStart, currentSelectionEnd); - } - - /** - * Invoked by the controller when the first result is received. - * - * @param {UrlbarResult} firstResult - * The first result received. - * @returns {boolean} - * True if this method canceled the query and started a new one. False - * otherwise. - */ - onFirstResult(firstResult) { - // If the heuristic result has a keyword but isn't a keyword offer, we may - // need to enter search mode. - if ( - firstResult.heuristic && - firstResult.payload.keyword && - !this.#providesSearchMode(firstResult) && - this.maybeConfirmSearchModeFromResult({ - result: firstResult, - entry: "typed", - checkValue: false, - }) - ) { - return true; - } - - // To prevent selection flickering, we apply autofill on input through a - // placeholder, without waiting for results. But, if the first result is - // not an autofill one, the autofill prediction was wrong and we should - // restore the original user typed string. - if (firstResult.autofill) { - this._autofillFirstResult(firstResult); - } else if ( - this._autofillPlaceholder && - // Avoid clobbering added spaces (for token aliases, for example). - !this.value.endsWith(" ") - ) { - this._autofillPlaceholder = null; - this._setValue(this.userTypedValue); - } - - return false; - } - - /** - * Starts a query based on the current input value. - * - * @param {object} [options] - * Object options - * @param {boolean} [options.allowAutofill] - * Whether or not to allow providers to include autofill results. - * @param {boolean} [options.autofillIgnoresSelection] - * Normally we autofill only if the cursor is at the end of the string, - * if this is set we'll autofill regardless of selection. - * @param {string} [options.searchString] - * The search string. If not given, the current input value is used. - * Otherwise, the current input value must start with this value. - * @param {boolean} [options.resetSearchState] - * If this is the first search of a user interaction with the input, set - * this to true (the default) so that search-related state from the previous - * interaction doesn't interfere with the new interaction. Otherwise set it - * to false so that state is maintained during a single interaction. The - * intended use for this parameter is that it should be set to false when - * this method is called due to input events. - * @param {event} [options.event] - * The user-generated event that triggered the query, if any. If given, we - * will record engagement event telemetry for the query. - */ - startQuery({ - allowAutofill, - autofillIgnoresSelection = false, - searchString, - resetSearchState = true, - event, - } = {}) { - if (!searchString) { - searchString = - this.getAttribute("pageproxystate") == "valid" ? "" : this.value; - } else if (!this.value.startsWith(searchString)) { - throw new Error("The current value doesn't start with the search string"); - } - - let queryContext = this.#makeQueryContext({ - allowAutofill, - event, - searchString, - }); - - if (event) { - this.controller.engagementEvent.start(event, queryContext, searchString); - } - - if (this._suppressStartQuery) { - return; - } - - this._autofillIgnoresSelection = autofillIgnoresSelection; - if (resetSearchState) { - this._resetSearchState(); - } - - if (this.searchMode) { - this.confirmSearchMode(); - } - - this._lastSearchString = searchString; - this._valueOnLastSearch = this.value; - - // TODO (Bug 1522902): This promise is necessary for tests, because some - // tests are not listening for completion when starting a query through - // other methods than startQuery (input events for example). - this.lastQueryContextPromise = this.controller.startQuery(queryContext); - } - - /** - * Sets the input's value, starts a search, and opens the view. - * - * @param {string} value - * The input's value will be set to this value, and the search will - * use it as its query. - * @param {object} [options] - * Object options - * @param {nsISearchEngine} [options.searchEngine] - * Search engine to use when the search is using a known alias. - * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry] - * If provided, we will record this parameter as the search mode entry point - * in Telemetry. Consumers should provide this if they expect their call - * to enter search mode. - * @param {boolean} [options.focus] - * If true, the urlbar will be focused. If false, the focus will remain - * unchanged. - * @param {boolean} [options.startQuery] - * If true, start query to show urlbar result by fireing input event. If - * false, not fire the event. - */ - search(value, options = {}) { - let { searchEngine, searchModeEntry, startQuery = true } = options; - if (options.focus ?? true) { - this.focus(); - } - let trimmedValue = value.trim(); - let end = trimmedValue.search(lazy.UrlUtils.REGEXP_SPACES); - let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end); - // Enter search mode if the string starts with a restriction token. - let searchMode = this.searchModeForToken(firstToken); - let firstTokenIsRestriction = !!searchMode; - if (!searchMode && searchEngine) { - searchMode = { engineName: searchEngine.name }; - firstTokenIsRestriction = searchEngine.aliases.includes(firstToken); - } - - if (searchMode) { - searchMode.entry = searchModeEntry; - this.searchMode = searchMode; - if (firstTokenIsRestriction) { - // Remove the restriction token/alias from the string to be searched for - // in search mode. - value = value.replace(firstToken, ""); - } - if (lazy.UrlUtils.REGEXP_SPACES.test(value[0])) { - // If there was a trailing space after the restriction token/alias, - // remove it. - value = value.slice(1); - } - } else if ( - Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken) - ) { - this.searchMode = null; - // If the entire value is a restricted token, append a space. - if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) { - value += " "; - } - } - this.#setInputValue(value); - // Avoid selecting the text if this method is called twice in a row. - this.selectionStart = -1; - - if (startQuery) { - // Note: proper IME Composition handling depends on the fact this generates - // an input event, rather than directly invoking the controller; everything - // goes through _on_input, that will properly skip the search until the - // composition is committed. _on_input also skips the search when it's the - // same as the previous search, but we want to allow consecutive searches - // with the same string. So clear _lastSearchString first. - this._lastSearchString = ""; - let event = new UIEvent("input", { - bubbles: true, - cancelable: false, - view: this.window, - detail: 0, - }); - this.inputField.dispatchEvent(event); - } - } - - /** - * Returns a search mode object if a token should enter search mode when - * typed. This does not handle engine aliases. - * - * @param {Values<typeof lazy.UrlbarTokenizer.RESTRICT>} token - * A restriction token to convert to search mode. - * @returns {?object} - * A search mode object. Null if search mode should not be entered. See - * setSearchMode documentation for details. - */ - searchModeForToken(token) { - if (token == lazy.UrlbarTokenizer.RESTRICT.SEARCH) { - return { - engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate) - ?.name, - }; - } - - let mode = - this.#isAddressbar && - lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token); - if (mode) { - // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES. - return { ...mode }; - } - - return null; - } - - /** - * Opens a search page if the value is non-empty, otherwise opens the - * search engine homepage (searchform). - * - * @param {string} value - * @param {object} options - * @param {nsISearchEngine} options.searchEngine - */ - openEngineHomePage(value, { searchEngine }) { - if (!searchEngine) { - console.warn("No searchEngine parameter"); - return; - } - - let trimmedValue = value.trim(); - let url; - if (trimmedValue) { - url = searchEngine.getSubmission(trimmedValue, null).uri.spec; - // TODO: record SAP telemetry, see Bug 1961789. - } else { - url = searchEngine.searchForm; - lazy.BrowserSearchTelemetry.recordSearchForm(searchEngine, this.#sapName); - } - - this._lastSearchString = ""; - if (this.#isAddressbar) { - this.#setInputValue(url); - } - this.selectionStart = -1; - - this.window.openTrustedLinkIn(url, "current"); - } - - /** - * Focus without the focus styles. - * This is used by Activity Stream and about:privatebrowsing for search hand-off. - */ - setHiddenFocus() { - this._hideFocus = true; - if (this.focused) { - this.removeAttribute("focused"); - } else { - this.focus(); - } - } - - /** - * Restore focus styles. - * This is used by Activity Stream and about:privatebrowsing for search hand-off. - * - * @param {boolean} forceSuppressFocusBorder - * Set true to suppress-focus-border attribute if this flag is true. - */ - removeHiddenFocus(forceSuppressFocusBorder = false) { - this._hideFocus = false; - if (this.focused) { - this.toggleAttribute("focused", true); - - if (forceSuppressFocusBorder) { - this.toggleAttribute("suppress-focus-border", true); - } - } - } - - /** - * Addressbar: Gets the search mode for a specific browser instance. - * Searchbar: Gets the window-global search mode. - * - * @param {MozBrowser} browser - * The search mode for this browser will be returned. - * @param {boolean} [confirmedOnly] - * Normally, if the browser has both preview and confirmed modes, preview - * mode will be returned since it takes precedence. If this argument is - * true, then only confirmed search mode will be returned, or null if - * search mode hasn't been confirmed. - * @returns {?object} - * A search mode object or null if the browser/window is not in search mode. - * See setSearchMode documentation. - */ - getSearchMode(browser, confirmedOnly = false) { - let modes = this.#getSearchModesObject(browser); - - // Return copies so that callers don't modify the stored values. - if (!confirmedOnly && modes.preview) { - return { ...modes.preview }; - } - if (modes.confirmed) { - return { ...modes.confirmed }; - } - return null; - } - - /** - * Addressbar: Sets the search mode for a specific browser instance. - * Searchbar: Sets the window-global search mode. - * If the given browser is selected, then this will also enter search mode. - * - * @param {object} searchMode - * A search mode object. - * @param {string} searchMode.engineName - * The name of the search engine to restrict to. - * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source - * A result source to restrict to. - * @param {string} searchMode.entry - * How search mode was entered. This is recorded in event telemetry. One of - * the values in UrlbarUtils.SEARCH_MODE_ENTRY. - * @param {boolean} [searchMode.isPreview] - * If true, we will preview search mode. Search mode preview does not record - * telemetry and has slighly different UI behavior. The preview is exited in - * favor of full search mode when a query is executed. False should be - * passed if the caller needs to enter search mode but expects it will not - * be interacted with right away. Defaults to true. - * @param {MozBrowser} browser - * The browser for which to set search mode. - * Pass the selected browser for the searchbar. - */ - async setSearchMode(searchMode, browser) { - if (this.#isSmartbarMode) { - return; - } - let currentSearchMode = this.getSearchMode(browser); - let areSearchModesSame = - (!currentSearchMode && !searchMode) || - lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode); - - // Exit search mode if the passed-in engine is invalid or hidden. - let engine; - if (searchMode?.engineName) { - if (!Services.search.isInitialized) { - await Services.search.init(); - } - engine = Services.search.getEngineByName(searchMode.engineName); - if (!engine || engine.hidden) { - searchMode = null; - } - } - - let { - engineName, - source, - entry, - restrictType, - isPreview = true, - } = searchMode || {}; - - searchMode = null; - - if (engineName) { - searchMode = { - engineName, - isGeneralPurposeEngine: engine.isGeneralPurposeEngine, - }; - if (source) { - searchMode.source = source; - } else if (searchMode.isGeneralPurposeEngine) { - // History results for general-purpose search engines are often not - // useful, so we hide them in search mode. See bug 1658646 for - // discussion. - searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH; - } - } else if (source) { - let sourceName = lazy.UrlbarUtils.getResultSourceName(source); - if (sourceName) { - searchMode = { source }; - } else { - console.error(`Unrecognized source: ${source}`); - } - } - - let modes = this.#getSearchModesObject(browser); - - if (searchMode) { - searchMode.isPreview = isPreview; - if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) { - searchMode.entry = entry; - } else { - // If we see this value showing up in telemetry, we should review - // search mode's entry points. - searchMode.entry = "other"; - } - - if (!searchMode.isPreview) { - modes.confirmed = searchMode; - delete modes.preview; - } else { - modes.preview = searchMode; - } - } else { - delete modes.preview; - delete modes.confirmed; - } - - if (restrictType) { - searchMode.restrictType = restrictType; - } - - // Enter search mode if the browser is selected. - if (browser == this.window.gBrowser.selectedBrowser) { - this._updateSearchModeUI(searchMode); - if (searchMode) { - // Set userTypedValue to the query string so that it's properly restored - // when switching back to the current tab and across sessions. - this.userTypedValue = this.untrimmedValue; - this.valueIsTyped = true; - if (!searchMode.isPreview && !areSearchModesSame) { - try { - lazy.BrowserSearchTelemetry.recordSearchMode(searchMode); - } catch (ex) { - console.error(ex); - } - } - } - } - } - - /** - * @typedef {object} SearchModesObject - * - * @property {object} [preview] preview search mode - * @property {object} [confirmed] confirmed search mode - */ - - /** - * @type {SearchModesObject|undefined} - * - * The (lazily initialized) search mode object for the searchbar. - * This is needed because the searchbar has one search mode per window that - * shouldn't change when switching tabs. For the address bar, the search mode - * is stored per browser in #browserStates and this is always undefined. - */ - #searchbarSearchModes; - - /** - * Addressbar: Gets the search modes object for a specific browser instance. - * Searchbar: Gets the window-global search modes object. - * - * @param {MozBrowser} browser - * The browser to get the search modes object for. - * Pass the selected browser for the searchbar. - * @returns {SearchModesObject} - */ - #getSearchModesObject(browser) { - if (!this.#isAddressbar) { - // The passed browser doesn't matter here, but it does in setSearchMode. - this.#searchbarSearchModes ??= {}; - return this.#searchbarSearchModes; - } - - let state = this.getBrowserState(browser); - state.searchModes ??= {}; - return state.searchModes; - } - - /** - * Restores the current browser search mode from a previously stored state. - */ - restoreSearchModeState() { - if (this.#isSmartbarMode) { - return; - } - let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); - this.searchMode = state.searchModes?.confirmed; - } - - /** - * Enters search mode with the default engine. - */ - searchModeShortcut() { - // We restrict to search results when entering search mode from this - // shortcut to honor historical behaviour. - this.searchMode = { - source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, - engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)?.name, - entry: "shortcut", - }; - // The searchMode setter clears the input if pageproxystate is valid, so - // we know at this point this.value will either be blank or the user's - // typed string. - this.search(this.value); - this.select(); - } - - /** - * Confirms the current search mode. - */ - confirmSearchMode() { - let searchMode = this.searchMode; - if (searchMode?.isPreview) { - searchMode.isPreview = false; - this.searchMode = searchMode; - - // Unselect the one-off search button to ensure UI consistency. - if (this.view.oneOffSearchButtons) { - this.view.oneOffSearchButtons.selectedButton = null; - } - } - } - - // Getters and Setters below. - - get editor() { - if (this.#isSmartbarMode) { - return this.#ensureSmartbarEditor(); - } - return this.inputField.editor; - } - - get focused() { - return ( - this.document.activeElement == - (this.#smartbarInputController?.input ?? this.inputField) - ); - } - - get goButton() { - return this.querySelector(".urlbar-go-button"); - } - - get value() { - return this.#smartbarInputController?.value ?? this.inputField.value; - } - - set value(val) { - this._setValue(val, { allowTrim: true }); - } - - get untrimmedValue() { - return this._untrimmedValue; - } - - get userTypedValue() { - return this.#isAddressbar - ? this.window.gBrowser.userTypedValue - : this._userTypedValue; - } - - set userTypedValue(val) { - if (this.#isAddressbar) { - this.window.gBrowser.userTypedValue = val; - } else { - this._userTypedValue = val; - } - } - - get lastSearchString() { - return this._lastSearchString; - } - - get searchMode() { - if (this.#isSmartbarMode) { - return null; - } - if (!this.window.gBrowser) { - // This only happens before DOMContentLoaded. - return null; - } - return this.getSearchMode(this.window.gBrowser.selectedBrowser); - } - - set searchMode(searchMode) { - if (this.#isSmartbarMode) { - return; - } - this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser); - this.searchModeSwitcher?.onSearchModeChanged(); - lazy.UrlbarSearchTermsPersistence.onSearchModeChanged(this.window); - } - - getBrowserState(browser) { - let state = this.#browserStates.get(browser); - if (!state) { - state = {}; - this.#browserStates.set(browser, state); - } - return state; - } - - async #updateLayoutBreakout() { - if (!this.#allowBreakout) { - return; - } - if (this.document.fullscreenElement) { - // Toolbars are hidden in DOM fullscreen mode, so we can't get proper - // layout information and need to retry after leaving that mode. - this.window.addEventListener( - "fullscreen", - () => { - this.#updateLayoutBreakout(); - }, - { once: true } - ); - return; - } - await this.#updateLayoutBreakoutDimensions(); - } - - startLayoutExtend() { - if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) { - // Do not expand if the Urlbar does not support being expanded or it is - // already expanded. - return; - } - if (!this.view.isOpen) { - return; - } - - this.#updateTextboxPosition(); - - this.setAttribute("breakout-extend", "true"); - - // Enable the animation only after the first extend call to ensure it - // doesn't run when opening a new window. - if (!this.hasAttribute("breakout-extend-animate")) { - this.window.promiseDocumentFlushed(() => { - this.window.requestAnimationFrame(() => { - this.setAttribute("breakout-extend-animate", "true"); - }); - }); - } - } - - endLayoutExtend() { - // If reduce motion is enabled, we want to collapse the Urlbar here so the - // user sees only sees two states: not expanded, and expanded with the view - // open. - if (!this.hasAttribute("breakout-extend") || this.view.isOpen) { - return; - } - - this.removeAttribute("breakout-extend"); - this.#updateTextboxPosition(); - } - - /** - * Updates the user interface to indicate whether the URI in the address bar - * is different than the loaded page, because it's being edited or because a - * search result is currently selected and is displayed in the location bar. - * - * @param {string} state - * The string "valid" indicates that the security indicators and other - * related user interface elments should be shown because the URI in - * the location bar matches the loaded page. The string "invalid" - * indicates that the URI in the location bar is different than the - * loaded page. - * @param {boolean} [updatePopupNotifications] - * Indicates whether we should update the PopupNotifications - * visibility due to this change, otherwise avoid doing so as it is - * being handled somewhere else. - * @param {boolean} [forceUnifiedSearchButtonAvailable] - * If this parameter is true, force to make Unified Search Button available. - * Otherwise, the availability will be depedent on the proxy state. - * Default value is false. - */ - setPageProxyState( - state, - updatePopupNotifications, - forceUnifiedSearchButtonAvailable = false - ) { - if (!this.#isAddressbar) { - return; - } - let prevState = this.getAttribute("pageproxystate"); - - this.setAttribute("pageproxystate", state); - this._inputContainer.setAttribute("pageproxystate", state); - this._identityBox?.setAttribute("pageproxystate", state); - this.setUnifiedSearchButtonAvailability( - forceUnifiedSearchButtonAvailable || state == "invalid" - ); - - if (state == "valid") { - this._lastValidURLStr = this.value; - } - - if ( - updatePopupNotifications && - prevState != state && - this.window.UpdatePopupNotificationsVisibility - ) { - this.window.UpdatePopupNotificationsVisibility(); - } - } - - /** - * When switching tabs quickly, TabSelect sometimes happens before - * _adjustFocusAfterTabSwitch and due to the focus still being on the old - * tab, we end up flickering the results pane briefly. - */ - afterTabSwitchFocusChange() { - this._gotFocusChange = true; - this._afterTabSelectAndFocusChange(); - } - - /** - * Confirms search mode and starts a new search if appropriate for the given - * result. See also _searchModeForResult. - * - * @param {object} options - * Options object. - * @param {string} [options.entry] - * If provided, this will be recorded as the entry point into search mode. - * See setSearchMode documentation for details. - * @param {UrlbarResult} [options.result] - * The result to confirm. Defaults to the currently selected result. - * @param {boolean} [options.checkValue] - * If true, the trimmed input value must equal the result's keyword in order - * to enter search mode. - * @param {boolean} [options.startQuery] - * If true, start a query after entering search mode. Defaults to true. - * @returns {boolean} - * True if we entered search mode and false if not. - */ - maybeConfirmSearchModeFromResult({ - entry, - result = this._resultForCurrentValue, - checkValue = true, - startQuery = true, - }) { - if ( - !result || - (checkValue && - this.value.trim() != result.payload.keyword?.trim() && - this.value.trim() != result.payload.autofillKeyword?.trim()) - ) { - return false; - } - - let searchMode = this._searchModeForResult(result, entry); - if (!searchMode) { - return false; - } - - this.searchMode = searchMode; - - let value = result.payload.query?.trimStart() || ""; - this._setValue(value); - - if (startQuery) { - this.startQuery({ allowAutofill: false }); - } - - return true; - } - - observe(subject, topic, data) { - switch (topic) { - case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: { - let engine = subject.QueryInterface(Ci.nsISearchEngine); - switch (data) { - case lazy.SearchUtils.MODIFIED_TYPE.CHANGED: - case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: { - let searchMode = this.searchMode; - if (searchMode?.engineName == engine.name) { - // Exit search mode if the current search mode engine was removed. - this.searchMode = searchMode; - } - break; - } - case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT: - if (!this.isPrivate) { - this._updatePlaceholder(engine.name); - } - break; - case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE: - if (this.isPrivate) { - this._updatePlaceholder(engine.name); - } - break; - } - break; - } - } - } - - /** - * Get search source. - * - * @param {Event} event - * The event that triggered this query. - * @returns {string} - * The source name. - */ - getSearchSource(event) { - if (this.#isAddressbar) { - if (this._isHandoffSession) { - return "urlbar-handoff"; - } - - const isOneOff = - this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event); - if (this.searchMode && !isOneOff) { - // Without checking !isOneOff, we might record the string - // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to - // oneoff_urlbar and oneoff_searchbar). The extra information is not - // necessary; the intent is the same regardless of whether the user is - // in search mode when they do a key-modified click/enter on a one-off. - return "urlbar-searchmode"; - } - - let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); - if (state.persist?.searchTerms && !isOneOff) { - // Normally, we use state.persist.shouldPersist to check if search terms - // persisted. However when the user modifies the search term, the boolean - // will become false. Thus, we check the presence of the search terms to - // know whether or not search terms ever persisted in the address bar. - return "urlbar-persisted"; - } - } - return this.#sapName; - } - - // Private methods below. - - /* - * Actions can have several buttons in the same result where not all - * will provide a searchMode so check the currently selected button - * in that case. - */ - #providesSearchMode(result) { - if (!result) { - return false; - } - if ( - this.view.selectedElement && - result.providerName == lazy.UrlbarProviderGlobalActions.name - ) { - return this.view.selectedElement.dataset.providesSearchmode == "true"; - } - return result.payload.providesSearchMode; - } - - _addObservers() { - this._observer ??= { - observe: this.observe.bind(this), - QueryInterface: ChromeUtils.generateQI([ - "nsIObserver", - "nsISupportsWeakReference", - ]), - }; - Services.obs.addObserver( - this._observer, - lazy.SearchUtils.TOPIC_ENGINE_MODIFIED, - true - ); - } - - _removeObservers() { - if (this._observer) { - Services.obs.removeObserver( - this._observer, - lazy.SearchUtils.TOPIC_ENGINE_MODIFIED - ); - this._observer = null; - } - } - - _getURIFixupInfo(searchString) { - let flags = - Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | - Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; - if (this.isPrivate) { - flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; - } - try { - return Services.uriFixup.getFixupURIInfo(searchString, flags); - } catch (ex) { - console.error( - `An error occured while trying to fixup "${searchString}"`, - ex - ); - } - return null; - } - - _afterTabSelectAndFocusChange() { - // We must have seen both events to proceed safely. - if (!this._gotFocusChange || !this._gotTabSelect) { - return; - } - this._gotFocusChange = this._gotTabSelect = false; - - this.formatValue(); - this._resetSearchState(); - - // We don't use the original TabSelect event because caching it causes - // leaks on MacOS. - const event = new CustomEvent("tabswitch"); - // If the urlbar is focused after a tab switch, record a potential - // engagement event. When switching from a focused to a non-focused urlbar, - // the blur event would record the abandonment. When switching from an - // unfocused to a focused urlbar, there should be no search session ongoing, - // so this will be a no-op. - if (this.focused) { - this.controller.engagementEvent.record(event, { - searchString: this._lastSearchString, - searchSource: this.getSearchSource(event), - }); - } - - // Switching tabs doesn't always change urlbar focus, so we must try to - // reopen here too, not just on focus. - if (this.view.autoOpen({ event })) { - return; - } - // The input may retain focus when switching tabs in which case we - // need to close the view and search mode switcher popup explicitly. - this.searchModeSwitcher.closePanel(); - this.view.close(); - } - - #updateTextboxPosition() { - if (!this.view.isOpen) { - this.style.top = ""; - return; - } - this.style.top = px( - this.parentNode.getBoxQuads({ - ignoreTransforms: true, - flush: false, - })[0].p1.y - ); - } - - #updateTextboxPositionNextFrame() { - if (!this.hasAttribute("breakout")) { - return; - } - // Allow for any layout changes to take place (e.g. when the menubar becomes - // inactive) before re-measuring to position the textbox - this.window.requestAnimationFrame(() => { - this.window.requestAnimationFrame(() => { - this.#updateTextboxPosition(); - }); - }); - } - - #stopBreakout() { - this.removeAttribute("breakout"); - this.parentNode.removeAttribute("breakout"); - this.style.top = ""; - try { - this.hidePopover(); - } catch (ex) { - // No big deal if not a popover already. - } - this._layoutBreakoutUpdateKey = {}; - } - - incrementBreakoutBlockerCount() { - this.#breakoutBlockerCount++; - if (this.#breakoutBlockerCount == 1) { - this.#stopBreakout(); - } - } - - decrementBreakoutBlockerCount() { - if (this.#breakoutBlockerCount > 0) { - this.#breakoutBlockerCount--; - } - if (this.#breakoutBlockerCount === 0) { - this.#updateLayoutBreakout(); - } - } - - async #updateLayoutBreakoutDimensions() { - this.#stopBreakout(); - - // When this method gets called a second time before the first call - // finishes, we need to disregard the first one. - let updateKey = {}; - this._layoutBreakoutUpdateKey = updateKey; - await this.window.promiseDocumentFlushed(() => {}); - await new Promise(resolve => { - this.window.requestAnimationFrame(() => { - if (this._layoutBreakoutUpdateKey != updateKey || !this.isConnected) { - return; - } - - this.parentNode.style.setProperty( - "--urlbar-container-height", - px(getBoundsWithoutFlushing(this.parentNode).height) - ); - this.style.setProperty( - "--urlbar-height", - px(getBoundsWithoutFlushing(this).height) - ); - - if (this.#breakoutBlockerCount) { - return; - } - - this.setAttribute("breakout", "true"); - this.parentNode.setAttribute("breakout", "true"); - this.showPopover(); - this.#updateTextboxPosition(); - - resolve(); - }); - }); - } - - /** - * Sets the input field value. - * - * @param {string} val The new value to set. - * @param {object} [options] Options for setting. - * @param {boolean} [options.allowTrim] Whether the value can be trimmed. - * @param {string} [options.untrimmedValue] Override for this._untrimmedValue. - * @param {boolean} [options.valueIsTyped] Override for this.valueIsTypede. - * @param {string} [options.actionType] Value for the `actiontype` attribute. - * - * @returns {string} The set value. - */ - _setValue( - val, - { - allowTrim = false, - untrimmedValue = null, - valueIsTyped = false, - actionType = undefined, - } = {} - ) { - // Don't expose internal about:reader URLs to the user. - let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val); - if (originalUrl) { - val = originalUrl.displaySpec; - } - this._untrimmedValue = untrimmedValue ?? val; - this._protocolIsTrimmed = false; - if (allowTrim) { - let oldVal = val; - val = this._trimValue(val); - this._protocolIsTrimmed = - oldVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) && - !val.startsWith(lazy.BrowserUIUtils.trimURLProtocol); - } - - this.valueIsTyped = valueIsTyped; - this._resultForCurrentValue = null; - this.#setInputValue(val); - this.formatValue(); - - if (actionType !== undefined) { - this.setAttribute("actiontype", actionType); - } else { - this.removeAttribute("actiontype"); - } - - // Dispatch ValueChange event for accessibility. - let event = this.document.createEvent("Events"); - event.initEvent("ValueChange", true, true); - this.inputField.dispatchEvent(event); - - return val; - } - - /** - * Extracts a input value from a UrlbarResult, used when filling the input - * field on selecting a result. - * - * Some examples: - * - If the result is a bookmark keyword or dynamic, the value will be - * its `input` property. - * - If the result is search, the value may be `keyword` combined with - * `suggestion` or `query`. - * - If the result is WebExtension Omnibox, the value will be extracted - * from `content`. - * - For results returning URLs the value may be `urlOverride` or `url`. - * - * @param {UrlbarResult} result - * The result to extract the value from. - * @param {object} options - * Options object. - * @param {string} [options.urlOverride] - * For results normally returning a url string, this allows to override - * it. A blank string may passed-in to clear the input. - * @param {HTMLElement} [options.element] - * The element that was selected or picked, if available. For results that - * have multiple selectable children, the value may be taken from a child - * element rather than the result. - * @returns {string} The value. - */ - #getValueFromResult(result, { urlOverride = null, element = null } = {}) { - switch (result.type) { - case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: - return result.payload.input; - case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: { - let value = ""; - if (result.payload.keyword) { - value += result.payload.keyword + " "; - } - value += result.payload.suggestion || result.payload.query; - return value; - } - case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: - return result.payload.content; - case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: - return ( - element?.dataset.query || - element?.dataset.url || - result.payload.input || - result.payload.query || - "" - ); - case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: - return result.payload.autofillKeyword + " "; - case lazy.UrlbarUtils.RESULT_TYPE.TIP: { - let value = element?.dataset.url || element?.dataset.input; - if (value) { - return value; - } - break; - } - } - - // Always respect a set urlOverride property. - if (urlOverride !== null) { - // This returns null for the empty string, allowing callers to clear the - // input by passing an empty string as urlOverride. - let url = URL.parse(urlOverride); - return url ? losslessDecodeURI(url.URI) : ""; - } - - let parsedUrl = URL.parse(result.payload.url); - // If the url is not parsable, just return an empty string; - if (!parsedUrl) { - return ""; - } - - let url = losslessDecodeURI(parsedUrl.URI); - // If the user didn't originally type a protocol, and we generated one, - // trim the http protocol from the input value, as https-first may upgrade - // it to https, breaking user expectations. - let stripHttp = - result.heuristic && - result.payload.url.startsWith("http://") && - this.userTypedValue && - this.#getSchemelessInput(this.userTypedValue) == - Ci.nsILoadInfo.SchemelessInputTypeSchemeless; - if (!stripHttp) { - return url; - } - // Attempt to trim the url. If doing so results in a string that is - // interpreted as search (e.g. unknown single word host, or domain suffix), - // use the unmodified url instead. Otherwise, if the user edits the url - // and confirms the new value, we may transform the url into a search. - let trimmedUrl = lazy.UrlbarUtils.stripPrefixAndTrim(url, { stripHttp })[0]; - let isSearch = !!this._getURIFixupInfo(trimmedUrl)?.keywordAsSent; - if (isSearch) { - // Although https-first might not respect the shown protocol, converting - // the result to a search would be more disruptive. - return url; - } - return trimmedUrl; - } - - /** - * Extracts from a result the value to use for the `actiontype` attribute. - * - * @param {UrlbarResult} result The UrlbarResult to consider. - * - * @returns {string} The `actiontype` value, or undefined. - */ - #getActionTypeFromResult(result) { - switch (result.type) { - case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: - return "switchtab"; - case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: - return "extension"; - default: - return undefined; - } - } - - /** - * Resets some state so that searches from the user's previous interaction - * with the input don't interfere with searches from a new interaction. - */ - _resetSearchState() { - this._lastSearchString = this.value; - this._autofillPlaceholder = null; - } - - /** - * Autofills the autofill placeholder string if appropriate, and determines - * whether autofill should be allowed for the new search started by an input - * event. - * - * @param {string} value - * The new search string. - * @returns {boolean} - * Whether autofill should be allowed in the new search. - */ - _maybeAutofillPlaceholder(value) { - // We allow autofill in local but not remote search modes. - let allowAutofill = - this.selectionEnd == value.length && - !this.searchMode?.engineName && - this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH; - - if (!allowAutofill) { - this.#clearAutofill(); - return false; - } - - // Determine whether we can autofill the placeholder. The placeholder is a - // value that we autofill now, when the search starts and before we wait on - // its first result, in order to prevent a flicker in the input caused by - // the previous autofilled substring disappearing and reappearing when the - // first result arrives. Of course we can only autofill the placeholder if - // it starts with the new search string, and we shouldn't autofill anything - // if the caret isn't at the end of the input. - let canAutofillPlaceholder = false; - if (this._autofillPlaceholder) { - if (this._autofillPlaceholder.type == "adaptive") { - canAutofillPlaceholder = - value.length >= - this._autofillPlaceholder.adaptiveHistoryInput.length && - this._autofillPlaceholder.value - .toLocaleLowerCase() - .startsWith(value.toLocaleLowerCase()); - } else { - canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL( - this._autofillPlaceholder.value, - value - ); - } - } - - if (!canAutofillPlaceholder) { - this._autofillPlaceholder = null; - } else if ( - this._autofillPlaceholder && - this.selectionEnd == this.value.length && - this._enableAutofillPlaceholder - ) { - let autofillValue = - value + this._autofillPlaceholder.value.substring(value.length); - if ( - this.value === autofillValue && - this.selectionStart === value.length && - this.selectionEnd === autofillValue.length - ) { - return true; - } - this._autofillValue({ - value: autofillValue, - selectionStart: value.length, - selectionEnd: autofillValue.length, - type: this._autofillPlaceholder.type, - adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput, - untrimmedValue: this._autofillPlaceholder.untrimmedValue, - }); - } - - return true; - } - - /** - * Invoked on overflow/underflow/scrollend events to update attributes - * related to the input text directionality. Overflow fade masks use these - * attributes to appear at the proper side of the urlbar. - */ - updateTextOverflow() { - if (!this._overflowing) { - this.removeAttribute("textoverflow"); - return; - } - - let isRTL = - this.getAttribute("domaindir") === "rtl" && - lazy.UrlbarUtils.isTextDirectionRTL(this.value, this.window); - - this.window.promiseDocumentFlushed(() => { - // Check overflow again to ensure it didn't change in the meanwhile. - let input = this.inputField; - if (input && this._overflowing) { - // Normally we overflow at the end side of the text direction, though - // RTL domains may cause us to overflow at the opposite side. - // The outcome differs depending on the input field contents and applied - // formatting, and reports the final state of all the scrolling into an - // attribute available to css rules. - // Note it's also possible to scroll an unfocused input field using - // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad - // scroll (without modifiers) on Mac. - let side = "both"; - if (isRTL) { - if (input.scrollLeft == 0) { - side = "left"; - } else if (input.scrollLeft == input.scrollLeftMin) { - side = "right"; - } - } else if (input.scrollLeft == 0) { - side = "right"; - } else if (input.scrollLeft == input.scrollLeftMax) { - side = "left"; - } - - this.window.requestAnimationFrame(() => { - // And check once again, since we might have stopped overflowing - // since the promiseDocumentFlushed callback fired. - if (this._overflowing) { - this.setAttribute("textoverflow", side); - } - }); - } - }); - } - - _updateUrlTooltip() { - if (this.focused || !this._overflowing) { - this.inputField.removeAttribute("title"); - } else { - this.inputField.setAttribute("title", this.untrimmedValue); - } - } - - _getSelectedValueForClipboard() { - let selectedVal = this.#selectedText; - - // Handle multiple-range selection as a string for simplicity. - if (this.editor.selection.rangeCount > 1) { - return selectedVal; - } - - // If the selection doesn't start at the beginning or doesn't span the - // full domain or the URL bar is modified or there is no text at all, - // nothing else to do here. - // TODO (Bug 1908360): the valueIsTyped usage here is confusing, as often - // it doesn't really indicate a user typed a value, it's rather used as - // a way to tell if the value was modified. - if ( - this.selectionStart > 0 || - selectedVal == "" || - (this.valueIsTyped && !this._protocolIsTrimmed) - ) { - return selectedVal; - } - - // The selection doesn't span the full domain if it doesn't contain a slash and is - // followed by some character other than a slash. - if (!selectedVal.includes("/")) { - let remainder = this.value.replace(selectedVal, ""); - if (remainder != "" && remainder[0] != "/") { - return selectedVal; - } - } - - let uri; - if (this.getAttribute("pageproxystate") == "valid") { - uri = this.#isOpenedPageInBlankTargetLoading - ? this.window.gBrowser.selectedBrowser.browsingContext - .nonWebControlledBlankURI - : this.window.gBrowser.currentURI; - } else { - // The value could be: - // 1. a trimmed url, set by selecting a result - // 2. a search string set by selecting a result - // 3. a url that was confirmed but didn't finish loading yet - // If it's an url the untrimmedValue should resolve to a valid URI, - // otherwise it's a search string that should be copied as-is. - - // If the copied text is that autofilled value, return the url including - // the protocol from its suggestion. - let result = this._resultForCurrentValue; - - if (result?.autofill?.value == selectedVal) { - return result.payload.url; - } - - uri = URL.parse(this._untrimmedValue)?.URI; - if (!uri) { - return selectedVal; - } - } - uri = this.makeURIReadable(uri); - let displaySpec = uri.displaySpec; - - // If the entire URL is selected, just use the actual loaded URI, - // unless we want a decoded URI, or it's a data: or javascript: URI, - // since those are hard to read when encoded. - if ( - this.value == selectedVal && - !uri.schemeIs("javascript") && - !uri.schemeIs("data") && - !lazy.UrlbarPrefs.get("decodeURLsOnCopy") - ) { - return displaySpec; - } - - // Just the beginning of the URL is selected, or we want a decoded - // url. First check for a trimmed value. - - if ( - !selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) && - // Note _trimValue may also trim a trailing slash, thus we can't just do - // a straight string compare to tell if the protocol was trimmed. - !displaySpec.startsWith(this._trimValue(displaySpec)) - ) { - selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal; - } - - // If selection starts from the beginning and part or all of the URL - // is selected, we check for decoded characters and encode them. - // Unless decodeURLsOnCopy is set. Do not encode data: URIs. - if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) { - try { - if (URL.canParse(selectedVal)) { - // Use encodeURI instead of URL.href because we don't want - // trailing slash. - selectedVal = encodeURI(selectedVal); - } - } catch (ex) { - // URL is invalid. Return original selected value. - } - } - - return selectedVal; - } - - _toggleActionOverride(event) { - if ( - event.keyCode == KeyEvent.DOM_VK_SHIFT || - event.keyCode == KeyEvent.DOM_VK_ALT || - event.keyCode == - (AppConstants.platform == "macosx" - ? KeyEvent.DOM_VK_META - : KeyEvent.DOM_VK_CONTROL) - ) { - if (event.type == "keydown") { - this._actionOverrideKeyCount++; - this.toggleAttribute("action-override", true); - this.view.panel.setAttribute("action-override", true); - } else if ( - this._actionOverrideKeyCount && - --this._actionOverrideKeyCount == 0 - ) { - this._clearActionOverride(); - } - } - } - - _clearActionOverride() { - this._actionOverrideKeyCount = 0; - this.removeAttribute("action-override"); - this.view.panel.removeAttribute("action-override"); - } - - /** - * Records in telemetry that a search is being loaded, - * updates an incremental total number of searches in a pref, - * and informs ASRouter that a search has occurred via a trigger send - * - * @param {nsISearchEngine} engine - * The engine to generate the query for. - * @param {Event} event - * The event that triggered this query. - * @param {object} [searchActionDetails] - * The details associated with this search query. - * @param {boolean} [searchActionDetails.isSuggestion] - * True if this query was initiated from a suggestion from the search engine. - * @param {boolean} [searchActionDetails.alias] - * True if this query was initiated via a search alias. - * @param {boolean} [searchActionDetails.isFormHistory] - * True if this query was initiated from a form history result. - * @param {string} [searchActionDetails.url] - * The url this query was triggered with. - * @param {MozBrowser} [browser] - * The browser where the search is being opened. - * Defaults to the window's selected browser. - */ - _recordSearch( - engine, - event, - searchActionDetails = {}, - browser = this.window.gBrowser.selectedBrowser - ) { - const isOneOff = this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event); - const searchSource = this.getSearchSource(event); - - // Record when the user uses the search bar to be - // used for message targeting. This is arbitrarily capped - // at 100, only to prevent the number from growing ifinitely. - const totalSearches = Services.prefs.getIntPref( - "browser.search.totalSearches" - ); - const totalSearchesCap = 100; - if (totalSearches < totalSearchesCap) { - Services.prefs.setIntPref( - "browser.search.totalSearches", - totalSearches + 1 - ); - } - - // Sending a trigger to ASRouter when a search happens - lazy.ASRouter.sendTriggerMessage({ - browser, - id: "onSearch", - context: { - isSuggestion: searchActionDetails.isSuggestion || false, - searchSource, - isOneOff, - }, - }); - - lazy.BrowserSearchTelemetry.recordSearch(browser, engine, searchSource, { - ...searchActionDetails, - isOneOff, - newtabSessionId: this._handoffSession, - }); - } - - /** - * Shortens the given value, usually by removing http:// and trailing slashes. - * - * @param {string} val - * The string to be trimmed if it appears to be URI - * @returns {string} - * The trimmed string - */ - _trimValue(val) { - if (!this.#isAddressbar) { - return val; - } - let trimmedValue = lazy.UrlbarPrefs.get("trimURLs") - ? lazy.BrowserUIUtils.trimURL(val) - : val; - // Only trim value if the directionality doesn't change to RTL and we're not - // showing a strikeout https protocol. - return lazy.UrlbarUtils.isTextDirectionRTL(trimmedValue, this.window) || - this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val) - ? val - : trimmedValue; - } - - /** - * Returns whether the passed-in event may represents a canonization request. - * - * @param {Event} event - * An Event to examine. - * @returns {boolean} - * Whether the event is a KeyboardEvent that triggers canonization. - */ - #isCanonizeKeyboardEvent(event) { - return ( - KeyboardEvent.isInstance(event) && - event.keyCode == KeyEvent.DOM_VK_RETURN && - (AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) && - !event._disableCanonization && - lazy.UrlbarPrefs.get("ctrlCanonizesURLs") - ); - } - - /** - * If appropriate, this prefixes a search string with 'www.' and suffixes it - * with browser.fixup.alternate.suffix prior to navigating. - * - * @param {Event} event - * The event that triggered this query. - * @param {string} value - * The search string that should be canonized. - * @returns {string} - * Returns the canonized URL if available and null otherwise. - */ - _maybeCanonizeURL(event, value) { - // Only add the suffix when the URL bar value isn't already "URL-like", - // and only if we get a keyboard event, to match user expectations. - if ( - this.sapName == "searchbar" || - !this.#isCanonizeKeyboardEvent(event) || - !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value) - ) { - return null; - } - - let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix"); - if (!suffix.endsWith("/")) { - suffix += "/"; - } - - // trim leading/trailing spaces (bug 233205) - value = value.trim(); - - // Tack www. and suffix on. If user has appended directories, insert - // suffix before them (bug 279035). Be careful not to get two slashes. - let firstSlash = value.indexOf("/"); - if (firstSlash >= 0) { - value = - value.substring(0, firstSlash) + - suffix + - value.substring(firstSlash + 1); - } else { - value = value + suffix; - } - - try { - const info = Services.uriFixup.getFixupURIInfo( - value, - Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI - ); - value = info.fixedURI.spec; - } catch (ex) { - console.error(`An error occured while trying to fixup "${value}"`, ex); - } - - this.value = value; - return value; - } - - /** - * Autofills a value into the input. The value will be autofilled regardless - * of the input's current value. - * - * @param {object} options - * The options object. - * @param {string} options.value - * The value to autofill. - * @param {number} options.selectionStart - * The new selectionStart. - * @param {number} options.selectionEnd - * The new selectionEnd. - * @param {"origin" | "url" | "adaptive"} options.type - * The autofill type, one of: "origin", "url", "adaptive" - * @param {string} options.adaptiveHistoryInput - * If the autofill type is "adaptive", this is the matching `input` value - * from adaptive history. - * @param {string} [options.untrimmedValue] - * Untrimmed value including a protocol. - */ - _autofillValue({ - value, - selectionStart, - selectionEnd, - type, - adaptiveHistoryInput, - untrimmedValue, - }) { - const valueMatches = this.value === value; - const selectionMatches = - this.selectionStart === selectionStart && - this.selectionEnd === selectionEnd; - if (valueMatches && selectionMatches) { - return; - } - // The autofilled value may be a URL that includes a scheme at the - // beginning. Do not allow it to be trimmed. - if (!valueMatches) { - this._setValue(value, { untrimmedValue }); - } - this.setSelectionRange(selectionStart, selectionEnd); - // Ensure selection state is cached for contenteditable and events fire. - if (!selectionMatches) { - if (this.#smartbarInputController) { - this.#smartbarInputController.dispatchSelectionChange(); - } else { - this.inputField.dispatchEvent( - new Event("selectionchange", { bubbles: true, cancelable: false }) - ); - } - } - this._autofillPlaceholder = { - value, - type, - adaptiveHistoryInput, - selectionStart, - selectionEnd, - untrimmedValue, - }; - } - - /** - * Called when a menu item from results menu is picked. - * - * @param {UrlbarResult} result The result that was picked. - * @param {Event} event The event that picked the result. - * @param {HTMLElement} element the picked view element, if available. - * @param {object} browser The browser to use for the load. - */ - #pickMenuResult(result, event, element, browser) { - this.controller.engagementEvent.record(event, { - result, - element, - searchString: this._lastSearchString, - selType: element.dataset.command, - }); - - if (element.dataset.command == "manage") { - this.window.openPreferences("search-locationBar"); - return; - } - - let url; - if (element.dataset.command == "help") { - url = result.payload.helpUrl; - } - url ||= element.dataset.url; - - if (!url) { - return; - } - - let where = this._whereToOpen(event); - if (element.dataset.command == "help" && where == "current") { - // Open help links in a new tab. - where = "tab"; - } - - this.view.close({ elementPicked: true }); - - this._loadURL( - url, - event, - where, - { - allowInheritPrincipal: false, - private: this.isPrivate, - }, - { - source: result.source, - type: result.type, - }, - browser - ); - } - - /** - * Loads the url in the appropriate place. - * - * @param {string} url - * The URL to open. - * @param {string} openUILinkWhere - * Where we expect the result to be opened. - * @param {object} params - * The parameters related to how and where the result will be opened. - * Further supported paramters are listed in _loadURL. - * @param {object} [params.triggeringPrincipal] - * The principal that the action was triggered from. - * @param {object} [resultDetails] - * Details of the selected result, if any. - * Further supported details are listed in _loadURL. - * @param {string} [resultDetails.searchTerm] - * Search term of the result source, if any. - * @param {object} browser the browser to use for the load. - */ - #prepareAddressbarLoad( - url, - openUILinkWhere, - params, - resultDetails = null, - browser - ) { - if (!this.#isAddressbar) { - throw new Error( - "Can't prepare addressbar load when this isn't an addressbar input" - ); - } - - // No point in setting these because we'll handleRevert() a few rows below. - if (openUILinkWhere == "current") { - // Make sure URL is formatted properly (don't show punycode). - let formattedURL = url; - try { - formattedURL = losslessDecodeURI(new URL(url).URI); - } catch {} - - this.value = - lazy.UrlbarPrefs.isPersistedSearchTermsEnabled() && - resultDetails?.searchTerm - ? resultDetails.searchTerm - : formattedURL; - browser.userTypedValue = this.value; - } - - // No point in setting this if we are loading in a new window. - if ( - openUILinkWhere != "window" && - this.window.gInitialPages.includes(url) - ) { - browser.initialPageLoadedFromUserAction = url; - } - - try { - lazy.UrlbarUtils.addToUrlbarHistory(url, this.window); - } catch (ex) { - // Things may go wrong when adding url to session history, - // but don't let that interfere with the loading of the url. - console.error(ex); - } - - // TODO: When bug 1498553 is resolved, we should be able to - // remove the !triggeringPrincipal condition here. - if ( - !params.triggeringPrincipal || - params.triggeringPrincipal.isSystemPrincipal - ) { - // Reset DOS mitigations for the basic auth prompt. - delete browser.authPromptAbuseCounter; - - // Reset temporary permissions on the current tab if the user reloads - // the tab via the urlbar. - if ( - openUILinkWhere == "current" && - browser.currentURI && - url === browser.currentURI.spec - ) { - this.window.SitePermissions.clearTemporaryBlockPermissions(browser); - } - } - - // Specifies that the URL load was initiated by the URL bar. - params.initiatedByURLBar = true; - } - - /** - * Loads the url in the appropriate place. - * - * @param {string} url - * The URL to open. - * @param {Event} event - * The event that triggered to load the url. - * @param {string} openUILinkWhere - * Where we expect the result to be opened. - * @param {object} params - * The parameters related to how and where the result will be opened. - * Further supported parameters are listed in utilityOverlay.js#openUILinkIn. - * @param {object} [params.triggeringPrincipal] - * The principal that the action was triggered from. - * @param {nsIInputStream} [params.postData] - * The POST data associated with a search submission. - * @param {boolean} [params.allowInheritPrincipal] - * Whether the principal can be inherited. - * @param {nsILoadInfo.SchemelessInputType} [params.schemelessInput] - * Whether the search/URL term was without an explicit scheme. - * @param {object} [resultDetails] - * Details of the selected result, if any. - * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} [resultDetails.type] - * Details of the result type, if any. - * @param {string} [resultDetails.searchTerm] - * Search term of the result source, if any. - * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} [resultDetails.source] - * Details of the result source, if any. - * @param {object} browser [optional] the browser to use for the load. - */ - _loadURL( - url, - event, - openUILinkWhere, - params, - resultDetails = null, - browser = this.window.gBrowser.selectedBrowser - ) { - if (this.#isAddressbar) { - this.#prepareAddressbarLoad( - url, - openUILinkWhere, - params, - resultDetails, - browser - ); - } - - params.allowThirdPartyFixup = true; - - if (openUILinkWhere == "current") { - params.targetBrowser = browser; - params.indicateErrorPageLoad = true; - params.allowPinnedTabHostChange = true; - params.allowPopups = url.startsWith("javascript:"); - } else { - params.initiatingDoc = this.window.document; - } - - if ( - this._keyDownEnterDeferred && - event?.keyCode === KeyEvent.DOM_VK_RETURN && - openUILinkWhere === "current" - ) { - // In this case, we move the focus to the browser that loads the content - // upon key up the enter key. - // To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid - // focusing on the browser in the function. And also, set loadedContent - // flag that whether the content is loaded in the current tab by this enter - // key. _keyDownEnterDeferred promise is processed at key up the enter, - // focus on the browser passed by _keyDownEnterDeferred.resolve(). - params.avoidBrowserFocus = true; - this._keyDownEnterDeferred.loadedContent = true; - this._keyDownEnterDeferred.resolve(browser); - } - - // Ensure the window gets the `private` feature if the current window - // is private, unless the caller explicitly requested not to. - if (this.isPrivate && !("private" in params)) { - params.private = true; - } - - // Focus the content area before triggering loads, since if the load - // occurs in a new tab, we want focus to be restored to the content - // area when the current tab is re-selected. - if (!params.avoidBrowserFocus) { - browser.focus(); - // Make sure the domain name stays visible for spoof protection and usability. - this.setSelectionRange(0, 0); - } - - if (openUILinkWhere != "current") { - this.handleRevert(); - } - - // Notify about the start of navigation. - this.#notifyStartNavigation(resultDetails); - - try { - this.window.openTrustedLinkIn(url, openUILinkWhere, params); - } catch (ex) { - // This load can throw an exception in certain cases, which means - // we'll want to replace the URL with the loaded URL: - if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) { - this.handleRevert(); - } - } - - // If we show the focus border after closing the view, it would appear to - // flash since this._on_blur would remove it immediately after. - this.view.close({ showFocusBorder: false }); - } - - /** - * Determines where a URL/page should be opened. - * - * @param {Event} event the event triggering the opening. - * @returns {"current" | "tabshifted" | "tab" | "save" | "window"} - */ - _whereToOpen(event) { - let isKeyboardEvent = KeyboardEvent.isInstance(event); - let reuseEmpty = isKeyboardEvent; - let where = undefined; - if ( - isKeyboardEvent && - (event.altKey || event.getModifierState("AltGraph")) - ) { - // We support using 'alt' to open in a tab, because ctrl/shift - // might be used for canonizing URLs: - where = event.shiftKey ? "tabshifted" : "tab"; - } else if (this.#isCanonizeKeyboardEvent(event)) { - // If we're allowing canonization, and this is a canonization key event, - // open in current tab to avoid handling as new tab modifier. - where = "current"; - } else { - where = lazy.BrowserUtils.whereToOpenLink(event, false, false); - } - if (lazy.UrlbarPrefs.get("openintab")) { - if (where == "current") { - where = "tab"; - } else if (where == "tab") { - where = "current"; - } - reuseEmpty = true; - } - if ( - where == "tab" && - reuseEmpty && - this.window.gBrowser.selectedTab.isEmpty - ) { - where = "current"; - } - return where; - } - - _initCopyCutController() { - // Clipboard handling is managed by the multiline editor in smartbar mode. - if (this.#isSmartbarMode) { - return; - } - if (this._copyCutController) { - return; - } - this._copyCutController = new CopyCutController(this); - this.inputField.controllers.insertControllerAt(0, this._copyCutController); - } - - /** - * Searches the context menu for the location of a specific command. - * - * @param {string} menuItemCommand - * The command to search for. - * @returns {HTMLElement} - * Html element that matches the command or - * the last element if we could not find the command. - */ - #findMenuItemLocation(menuItemCommand) { - let inputBox = this.querySelector("moz-input-box"); - let contextMenu = inputBox.menupopup; - let insertLocation = contextMenu.firstElementChild; - // find the location of the command - while ( - insertLocation.nextElementSibling && - insertLocation.getAttribute("cmd") != menuItemCommand - ) { - insertLocation = insertLocation.nextElementSibling; - } - - return insertLocation; - } - - /** - * Strips known tracking query parameters/ link decorators. - * - * @returns {nsIURI} - * The stripped URI or original URI, if nothing can be - * stripped - */ - #stripURI() { - let copyString = this._getSelectedValueForClipboard(); - if (!copyString) { - return null; - } - let strippedURI = null; - - // Error check occurs during isClipboardURIValid - let uri = Services.io.newURI(copyString); - try { - strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri); - } catch (e) { - console.warn(`stripForCopyOrShare: ${e.message}`); - return uri; - } - - if (strippedURI) { - return this.makeURIReadable(strippedURI); - } - return uri; - } - - /** - * Checks if the clipboard contains a valid URI - * - * @returns {true|false} - */ - #isClipboardURIValid() { - let copyString = this._getSelectedValueForClipboard(); - if (!copyString) { - return false; - } - - return URL.canParse(copyString); - } - - /** - * Checks if there is a query parameter that can be stripped - * - * @returns {true|false} - */ - #canStrip() { - let copyString = this._getSelectedValueForClipboard(); - if (!copyString) { - return false; - } - // throws if the selected string is not a valid URI - try { - let uri = Services.io.newURI(copyString); - return lazy.QueryStringStripper.canStripForShare(uri); - } catch (e) { - console.warn("canStrip failed!", e); - return false; - } - } - - /** - * Restores the untrimmed value in the urlbar. - * - * @param {object} [options] - * Options for untrimming. - * @param {boolean} [options.moveCursorToStart] - * Whether the cursor should be moved at position 0 after untrimming. - * @param {boolean} [options.ignoreSelection] - * Whether this should untrim, regardless of the current selection state. - */ - #maybeUntrimUrl({ moveCursorToStart = false, ignoreSelection = false } = {}) { - // Check if we can untrim the current value. - if ( - !lazy.UrlbarPrefs.getScotchBonnetPref( - "untrimOnUserInteraction.featureGate" - ) || - !this._protocolIsTrimmed || - !this.focused || - (!ignoreSelection && this.#allTextSelected) - ) { - return; - } - - let selectionStart = this.selectionStart; - let selectionEnd = this.selectionEnd; - - // Correct the selection taking the trimmed protocol into account. - let offset = lazy.BrowserUIUtils.trimURLProtocol.length; - - // In case of autofill, we may have to adjust its boundaries. - if (this._autofillPlaceholder) { - this._autofillPlaceholder.selectionStart += offset; - this._autofillPlaceholder.selectionEnd += offset; - } - - if (moveCursorToStart) { - this._setValue(this._untrimmedValue, { - valueIsTyped: this.valueIsTyped, - }); - this.setSelectionRange(0, 0); - return; - } - - if (selectionStart == selectionEnd) { - // When cursor is at the end of the string, untrimming may - // reintroduced a trailing slash and we want to move past it. - if (selectionEnd == this.value.length) { - offset += 1; - } - selectionStart = selectionEnd += offset; - } else { - // There's a selection, so we must calculate both the initial - // protocol and the eventual trailing slash. - if (selectionStart != 0) { - selectionStart += offset; - } else { - // When selection starts at the beginning, the adjusted selection will - // include the protocol only if the selected text includes the host. - // The port is left out, as one may want to exclude it from the copy. - let prePathMinusPort; - try { - let uri = Services.io.newURI(this._untrimmedValue); - prePathMinusPort = [uri.userPass, uri.displayHost] - .filter(Boolean) - .join("@"); - } catch (ex) { - lazy.logger.error("Should only try to untrim valid URLs"); - } - if (!this.#selectedText.startsWith(prePathMinusPort)) { - selectionStart += offset; - } - } - if (selectionEnd == this.value.length) { - offset += 1; - } - selectionEnd += offset; - } - - this._setValue(this._untrimmedValue, { - valueIsTyped: this.valueIsTyped, - }); - - this.setSelectionRange(selectionStart, selectionEnd); - } - - // The strip-on-share feature will strip known tracking/decorational - // query params from the URI and copy the stripped version to the clipboard. - _initStripOnShare() { - let contextMenu = this.querySelector("moz-input-box").menupopup; - let insertLocation = this.#findMenuItemLocation("cmd_copy"); - // set up the menu item - let stripOnShare = this.document.createXULElement("menuitem"); - this.document.l10n.setAttributes( - stripOnShare, - "text-action-copy-clean-link" - ); - stripOnShare.setAttribute("anonid", "strip-on-share"); - stripOnShare.id = "strip-on-share"; - - insertLocation.insertAdjacentElement("afterend", stripOnShare); - - // Register listener that returns the stripped url or falls back - // to the original url if nothing can be stripped. - stripOnShare.addEventListener("command", () => { - let strippedURI = this.#stripURI(); - lazy.ClipboardHelper.copyString(strippedURI.displaySpec); - }); - - // Register a listener that hides the menu item if there is nothing to copy. - contextMenu.addEventListener("popupshowing", () => { - // feature is not enabled - if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) { - stripOnShare.setAttribute("hidden", true); - return; - } - let controller = - this.document.commandDispatcher.getControllerForCommand("cmd_copy"); - if ( - !controller.isCommandEnabled("cmd_copy") || - !this.#isClipboardURIValid() - ) { - stripOnShare.setAttribute("hidden", true); - return; - } - stripOnShare.removeAttribute("hidden"); - if (!this.#canStrip()) { - stripOnShare.setAttribute("disabled", true); - return; - } - stripOnShare.removeAttribute("disabled"); - }); - } - - _initPasteAndGo() { - let inputBox = this.querySelector("moz-input-box"); - let contextMenu = inputBox.menupopup; - let insertLocation = this.#findMenuItemLocation("cmd_paste"); - if (!insertLocation) { - return; - } - - let pasteAndGo = this.document.createXULElement("menuitem"); - pasteAndGo.id = "paste-and-go"; - let label = Services.strings - .createBundle("chrome://browser/locale/browser.properties") - .GetStringFromName("pasteAndGo.label"); - pasteAndGo.setAttribute("label", label); - pasteAndGo.setAttribute("anonid", "paste-and-go"); - pasteAndGo.addEventListener("command", () => { - this._suppressStartQuery = true; - - this.select(); - this.window.goDoCommand("cmd_paste"); - this.setResultForCurrentValue(null); - this.handleCommand(); - this.controller.clearLastQueryContextCache(); - - this._suppressStartQuery = false; - }); - - contextMenu.addEventListener("popupshowing", () => { - // Close the results pane when the input field contextual menu is open, - // because paste and go doesn't want a result selection. - this.view.close(); - - let controller = - this.document.commandDispatcher.getControllerForCommand("cmd_paste"); - let enabled = controller.isCommandEnabled("cmd_paste"); - if (enabled) { - pasteAndGo.removeAttribute("disabled"); - } else { - pasteAndGo.setAttribute("disabled", "true"); - } - }); - - insertLocation.insertAdjacentElement("afterend", pasteAndGo); - } - - /** - * This notifies observers that the user has entered or selected something in - * the URL bar which will cause navigation. - * - * We use the observer service, so that we don't need to load extra facilities - * if they aren't being used, e.g. WebNavigation. - * - * @param {UrlbarResult} result - * Details of the result that was selected, if any. - */ - #notifyStartNavigation(result) { - if (this.#isAddressbar) { - Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation"); - } - } - - /** - * Returns a search mode object if a result should enter search mode when - * selected. - * - * @param {UrlbarResult} result - * The result to check. - * @param {string} [entry] - * If provided, this will be recorded as the entry point into search mode. - * See setSearchMode() documentation for details. - * @returns {object} A search mode object. Null if search mode should not be - * entered. See setSearchMode documentation for details. - */ - _searchModeForResult(result, entry = null) { - // Search mode is determined by the result's keyword or engine. - if (!result.payload.keyword && !result.payload.engine) { - return null; - } - - let searchMode = this.searchModeForToken(result.payload.keyword); - // If result.originalEngine is set, then the user is Alt+Tabbing - // through the one-offs, so the keyword doesn't match the engine. - if ( - !searchMode && - result.payload.engine && - (!result.payload.originalEngine || - result.payload.engine == result.payload.originalEngine) - ) { - searchMode = { engineName: result.payload.engine }; - } - - if (searchMode) { - if (result.type == lazy.UrlbarUtils.RESULT_TYPE.RESTRICT) { - searchMode.restrictType = "keyword"; - } else if ( - lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(result.payload.keyword) - ) { - searchMode.restrictType = "symbol"; - } - if (entry) { - searchMode.entry = entry; - } else { - switch (result.providerName) { - case "UrlbarProviderTopSites": - searchMode.entry = "topsites_urlbar"; - break; - case "UrlbarProviderTabToSearch": - if (result.payload.dynamicType) { - searchMode.entry = "tabtosearch_onboard"; - } else { - searchMode.entry = "tabtosearch"; - } - break; - default: - searchMode.entry = "keywordoffer"; - break; - } - } - } - - return searchMode; - } - - /** - * Updates the UI so that search mode is either entered or exited. - * - * @param {object} searchMode - * See setSearchMode documentation. If null, then search mode is exited. - */ - _updateSearchModeUI(searchMode) { - let { engineName, source, isGeneralPurposeEngine } = searchMode || {}; - - // As an optimization, bail if the given search mode is null but search mode - // is already inactive. Otherwise, browser_preferences_usage.js fails due to - // accessing the browser.urlbar.placeholderName pref (via the call to - // initPlaceHolder below) too many times. That test does not enter search mode, - // but it triggers many calls to this method with a null search mode, via setURI. - if (!engineName && !source && !this.hasAttribute("searchmode")) { - return; - } - - if (this._searchModeIndicatorTitle) { - this._searchModeIndicatorTitle.textContent = ""; - this._searchModeIndicatorTitle.removeAttribute("data-l10n-id"); - } - - if (!engineName && !source) { - this.removeAttribute("searchmode"); - this.initPlaceHolder(true); - return; - } - - if (this.#isAddressbar) { - if (engineName) { - // Set text content for the search mode indicator. - this._searchModeIndicatorTitle.textContent = engineName; - this.document.l10n.setAttributes( - this.inputField, - isGeneralPurposeEngine - ? "urlbar-placeholder-search-mode-web-2" - : "urlbar-placeholder-search-mode-other-engine", - { name: engineName } - ); - } else if (source) { - const messageIDs = { - actions: "urlbar-placeholder-search-mode-other-actions", - bookmarks: "urlbar-placeholder-search-mode-other-bookmarks", - engine: "urlbar-placeholder-search-mode-other-engine", - history: "urlbar-placeholder-search-mode-other-history", - tabs: "urlbar-placeholder-search-mode-other-tabs", - }; - let sourceName = lazy.UrlbarUtils.getResultSourceName(source); - let l10nID = `urlbar-search-mode-${sourceName}`; - this.document.l10n.setAttributes( - this._searchModeIndicatorTitle, - l10nID - ); - this.document.l10n.setAttributes( - this.inputField, - messageIDs[sourceName] - ); - } - } - - this.toggleAttribute("searchmode", true); - // Clear autofill. - if (this._autofillPlaceholder && this.userTypedValue) { - this.value = this.userTypedValue; - } - // Search mode should only be active when pageproxystate is invalid. - if (this.getAttribute("pageproxystate") == "valid") { - this.value = ""; - this.setPageProxyState("invalid", true); - } - - this.searchModeSwitcher?.onSearchModeChanged(); - } - - /** - * Handles persisted search terms logic for the current browser. This manages - * state and updates the UI accordingly. - * - * @param {object} options - * @param {object} options.state - * The state object for the currently viewed browser. - * @param {boolean} options.hideSearchTerms - * True if we must hide the search terms and instead show the page URL. - * @param {boolean} options.dueToTabSwitch - * True if the browser was revealed again due to a tab switch. - * @param {boolean} options.isSameDocument - * True if the page load was same document. - * @param {nsIURI} [options.uri] - * The latest URI of the page. - * @returns {boolean} - * Whether search terms should persist. - */ - #handlePersistedSearchTerms({ - state, - hideSearchTerms, - dueToTabSwitch, - isSameDocument, - uri, - }) { - if (!this.#isAddressbar) { - return false; - } - if (!lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()) { - if (state.persist) { - this.removeAttribute("persistsearchterms"); - delete state.persist; - } - return false; - } - - // The first time the browser URI has been loaded to the input. If - // persist is not defined, it is likely due to the tab being created in - // the background or an existing tab moved to a new window and we have to - // do the work for the first time. - let firstView = (!isSameDocument && !dueToTabSwitch) || !state.persist; - - let cachedUriDidChange = - state.persist?.originalURI && - (!this.window.gBrowser.selectedBrowser.originalURI || - !state.persist.originalURI.equals( - this.window.gBrowser.selectedBrowser.originalURI - )); - - // Capture the shouldPersist property if it exists before - // setPersistenceState potentially modifies it. - let wasPersisting = state.persist?.shouldPersist ?? false; - - if (firstView || cachedUriDidChange) { - lazy.UrlbarSearchTermsPersistence.setPersistenceState( - state, - this.window.gBrowser.selectedBrowser.originalURI - ); - } - let shouldPersist = - !hideSearchTerms && - lazy.UrlbarSearchTermsPersistence.shouldPersist(state, { - dueToTabSwitch, - isSameDocument, - uri: uri ?? this.window.gBrowser.currentURI, - userTypedValue: this.userTypedValue, - firstView, - }); - // When persisting, userTypedValue should have a value consistent with the - // search terms to mimic a user typing the search terms. - // When turning off persist, check if the userTypedValue needs to be - // removed in order for the URL to return to the address bar. Single page - // application SERPs will load secondary search pages (e.g. Maps, Images) - // with the same document, which won't unset userTypedValue. - if (shouldPersist) { - this.userTypedValue = state.persist.searchTerms; - } else if (wasPersisting && !shouldPersist) { - this.userTypedValue = null; - } - - state.persist.shouldPersist = shouldPersist; - this.toggleAttribute("persistsearchterms", state.persist.shouldPersist); - - if (state.persist.shouldPersist && !isSameDocument) { - Glean.urlbarPersistedsearchterms.viewCount.add(1); - } - - return shouldPersist; - } - - /** - * Initializes the urlbar placeholder to the pre-saved engine name. We do this - * via a preference, to avoid needing to synchronously init the search service. - * - * This should be called around the time of DOMContentLoaded, so that it is - * initialized quickly before the user sees anything. - * - * Note: If the preference doesn't exist, we don't do anything as the default - * placeholder is a string which doesn't have the engine name; however, this - * can be overridden using the `force` parameter. - * - * @param {boolean} force If true and the preference doesn't exist, the - * placeholder will be set to the default version - * without an engine name ("Search or enter address"). - */ - initPlaceHolder(force = false) { - if (!this.#isAddressbar) { - return; - } - - let prefName = - "browser.urlbar.placeholderName" + (this.isPrivate ? ".private" : ""); - let engineName = Services.prefs.getStringPref(prefName, ""); - if (engineName || force) { - // We can do this directly, since we know we're at DOMContentLoaded. - this._setPlaceholder(engineName || null); - } - } - - /** - * Asynchronously changes the urlbar placeholder to the name of the default - * engine according to the search service when it is initialized. - * - * This should be called around the time of MozAfterPaint. Since the - * placeholder was already initialized to the pre-saved engine name by - * initPlaceHolder when this is called, the update is delayed to avoid - * confusing the user. - */ - async delayedStartupInit() { - // Only delay if requested, and we're not displaying text in the URL bar - // currently. - if (!this.value) { - // Delays changing the URL Bar placeholder and Unified Search Button icon - // until the user is not going to be seeing it, e.g. when there is a value - // entered in the bar, or if there is a tab switch to a tab which has a url - // loaded. We delay the update until the user is out of search mode since - // an alternative placeholder is used in search mode. - let updateListener = () => { - if (this.value && !this.searchMode) { - // By the time the user has switched, they may have changed the engine - // again, so we need to call this function again but with the - // new engine name. - // No need to await for this to finish, we're in a listener here anyway. - this.searchModeSwitcher.updateSearchIcon(); - this._updatePlaceholderFromDefaultEngine(); - this.inputField.removeEventListener("input", updateListener); - this.window.gBrowser.tabContainer.removeEventListener( - "TabSelect", - updateListener - ); - } - }; - - this.inputField.addEventListener("input", updateListener); - this.window.gBrowser.tabContainer.addEventListener( - "TabSelect", - updateListener - ); - } else { - await this._updatePlaceholderFromDefaultEngine(); - } - - // If we haven't finished initializing, ensure the placeholder - // preference is set for the next startup. - if (this.#isAddressbar) { - lazy.SearchUIUtils.updatePlaceholderNamePreference( - await this._getDefaultSearchEngine(), - this.isPrivate - ); - } - } - - /** - * Set Unified Search Button availability. - * - * @param {boolean} available If true Unified Search Button will be available. - */ - setUnifiedSearchButtonAvailability(available) { - if (this.#isSmartbarMode) { - return; - } - this.toggleAttribute("unifiedsearchbutton-available", available); - this.getBrowserState( - this.window.gBrowser.selectedBrowser - ).isUnifiedSearchButtonAvailable = available; - } - - /** - * Returns a Promise that resolves with default search engine. - * - * @returns {Promise<nsISearchEngine>} - */ - _getDefaultSearchEngine() { - return this.isPrivate - ? Services.search.getDefaultPrivate() - : Services.search.getDefault(); - } - - /** - * This is a wrapper around '_updatePlaceholder' that uses the appropriate - * default engine to get the engine name. - */ - async _updatePlaceholderFromDefaultEngine() { - const defaultEngine = await this._getDefaultSearchEngine(); - this._updatePlaceholder(defaultEngine.name); - } - - /** - * Updates the URLBar placeholder for the specified engine, delaying the - * update if required. - * - * Note: The engine name will only be displayed for application-provided - * engines, as we know they should have short names. - * - * @param {string} engineName The search engine name to use for the update. - */ - _updatePlaceholder(engineName) { - if (!engineName) { - throw new Error("Expected an engineName to be specified"); - } - - if (this.searchMode || !this.#isAddressbar) { - return; - } - - let engine = Services.search.getEngineByName(engineName); - if (engine.isConfigEngine) { - this._setPlaceholder(engineName); - } else { - // Display the default placeholder string. - this._setPlaceholder(null); - } - } - - /** - * Sets the URLBar placeholder to either something based on the engine name, - * or the default placeholder. - * - * @param {?string} engineName - * The name of the engine or null to use the default placeholder. - */ - _setPlaceholder(engineName) { - if (this.#isSmartbarMode) { - this.document.l10n.setAttributes(this.inputField, "smartbar-placeholder"); - return; - } - - if (!this.#isAddressbar) { - this.document.l10n.setAttributes(this.inputField, "searchbar-input"); - return; - } - - let l10nId; - if (lazy.UrlbarPrefs.get("keyword.enabled")) { - l10nId = engineName - ? "urlbar-placeholder-with-name" - : "urlbar-placeholder"; - } else { - l10nId = "urlbar-placeholder-keyword-disabled"; - } - - this.document.l10n.setAttributes( - this.inputField, - l10nId, - l10nId == "urlbar-placeholder-with-name" - ? { name: engineName } - : undefined - ); - } - - /** - * Determines if we should select all the text in the Urlbar based on the - * Urlbar state, and whether the selection is empty. - */ - #maybeSelectAll() { - if ( - !this._preventClickSelectsAll && - this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING && - this.focused && - this.selectionStart == this.selectionEnd - ) { - this.select(); - } - } - - // Event handlers below. - - _on_command(event) { - // Something is executing a command, likely causing a focus change. This - // should not be recorded as an abandonment. If the user is selecting a - // result menu item or entering search mode from a one-off, then they are - // in the same engagement and we should not discard. - if ( - !event.target.classList.contains("urlbarView-result-menuitem") && - (!event.target.classList.contains("searchbar-engine-one-off-item") || - this.searchMode?.entry != "oneoff") - ) { - this.controller.engagementEvent.discard(); - } - } - - _on_blur(event) { - lazy.logger.debug("Blur Event"); - // We cannot count every blur events after a missed engagement as abandoment - // because the user may have clicked on some view element that executes - // a command causing a focus change. For example opening preferences from - // the oneoff settings button. - // For now we detect that case by discarding the event on command, but we - // may want to figure out a more robust way to detect abandonment. - this.controller.engagementEvent.record(event, { - searchString: this._lastSearchString, - searchSource: this.getSearchSource(event), - }); - - this.focusedViaMousedown = false; - this._handoffSession = undefined; - this._isHandoffSession = false; - this.removeAttribute("focused"); - - if (this._autofillPlaceholder && this.userTypedValue) { - // If we were autofilling, remove the autofilled portion, by restoring - // the value to the last typed one. - this.value = this.userTypedValue; - } else if ( - this.value == this._untrimmedValue && - !this.userTypedValue && - !this.focused - ) { - // If the value was untrimmed by _on_focus and didn't change, trim it. - this.value = this._untrimmedValue; - } else { - // We're not updating the value, so just format it. - this.formatValue(); - } - - this._resetSearchState(); - - // In certain cases, like holding an override key and confirming an entry, - // we don't key a keyup event for the override key, thus we make this - // additional cleanup on blur. - this._clearActionOverride(); - - // The extension input sessions depends more on blur than on the fact we - // actually cancel a running query, so we do it here. - if (lazy.ExtensionSearchHandler.hasActiveInputSession()) { - lazy.ExtensionSearchHandler.handleInputCancelled(); - } - - // Respect the autohide preference for easier inspecting/debugging via - // the browser toolbox. - if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { - this.view.close(); - } - - // We may have hidden popup notifications, show them again if necessary. - if ( - this.getAttribute("pageproxystate") != "valid" && - this.window.UpdatePopupNotificationsVisibility - ) { - this.window.UpdatePopupNotificationsVisibility(); - } - - // If user move the focus to another component while pressing Enter key, - // then keyup at that component, as we can't get the event, clear the promise. - if (this._keyDownEnterDeferred) { - this._keyDownEnterDeferred.resolve(); - this._keyDownEnterDeferred = null; - } - this._isKeyDownWithCtrl = false; - this._isKeyDownWithMeta = false; - this._isKeyDownWithMetaAndLeft = false; - - Services.obs.notifyObservers(null, "urlbar-blur"); - } - - _on_click(event) { - switch (event.target) { - case this.inputField: - case this._inputContainer: - this.#maybeSelectAll(); - this.#maybeUntrimUrl(); - break; - - case this._searchModeIndicatorClose: - if (event.button != 2) { - this.searchMode = null; - if (this.view.oneOffSearchButtons) { - this.view.oneOffSearchButtons.selectedButton = null; - } - if (this.view.isOpen) { - this.startQuery({ - event, - }); - } - } - break; - - case this._revertButton: - this.handleRevert(); - this.select(); - break; - - case this.goButton: - this.handleCommand(event); - break; - } - } - - _on_contextmenu(event) { - this.#lazy.addSearchEngineHelper.refreshContextMenu(event); - - // Context menu opened via keyboard shortcut. - if (!event.button) { - return; - } - - this.#maybeSelectAll(); - } - - _on_focus(event) { - lazy.logger.debug("Focus Event"); - if (!this._hideFocus) { - this.toggleAttribute("focused", true); - } - - // If the value was trimmed, check whether we should untrim it. - // This is necessary when a protocol was typed, but the whole url has - // invalid parts, like the origin, then editing and confirming the trimmed - // value would execute a search instead of visiting the typed url. - if (this._protocolIsTrimmed) { - let untrim = false; - let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI; - if (fixedURI) { - try { - let expectedURI = Services.io.newURI(this._untrimmedValue); - if ( - lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps") && - this._untrimmedValue.startsWith("https://") - ) { - untrim = - fixedURI.displaySpec.replace("http://", "https://") != - expectedURI.displaySpec; // FIXME bug 1847723: Figure out a way to do this without manually messing with the fixed up URI. - } else { - untrim = fixedURI.displaySpec != expectedURI.displaySpec; - } - } catch (ex) { - untrim = true; - } - } - if (untrim) { - this._setValue(this._untrimmedValue); - } - } - - if (this.focusedViaMousedown) { - this.view.autoOpen({ event }); - } else { - if (this._untrimOnFocusAfterKeydown) { - // While the mousedown focus has more complex implications due to drag - // and double-click select, we can untrim immediately when the urlbar is - // focused by a keyboard shortcut. - this.#maybeUntrimUrl({ ignoreSelection: true }); - } - - if (this.inputField.hasAttribute("refocused-by-panel")) { - this.#maybeSelectAll(); - } - } - - this._updateUrlTooltip(); - this.formatValue(); - - // Hide popup notifications, to reduce visual noise. - if ( - this.getAttribute("pageproxystate") != "valid" && - this.window.UpdatePopupNotificationsVisibility - ) { - this.window.UpdatePopupNotificationsVisibility(); - } - - Services.obs.notifyObservers(null, "urlbar-focus"); - } - - _on_mouseover() { - this._updateUrlTooltip(); - } - - _on_draggableregionleftmousedown() { - if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { - this.view.close(); - } - } - - _on_mousedown(event) { - switch (event.currentTarget) { - case this: { - this._mousedownOnUrlbarDescendant = true; - if ( - event.composedTarget != this.inputField && - event.composedTarget != this._inputContainer - ) { - break; - } - - this.focusedViaMousedown = !this.focused; - this._preventClickSelectsAll = this.focused; - - // Keep the focus status, since the attribute may be changed - // upon calling this.focus(). - const hasFocus = this.hasAttribute("focused"); - if (event.composedTarget != this.inputField) { - this.focus(); - } - - // The rest of this case only cares about left clicks. - if (event.button != 0) { - break; - } - - // Clear any previous selection unless we are focused, to ensure it - // doesn't affect drag selection. - if (this.focusedViaMousedown) { - this.setSelectionRange(0, 0); - } - - // Do not suppress the focus border if we are already focused. If we - // did, we'd hide the focus border briefly then show it again if the - // user has Top Sites disabled, creating a flashing effect. - this.view.autoOpen({ - event, - suppressFocusBorder: !hasFocus, - }); - break; - } - case this.window: - if (this._mousedownOnUrlbarDescendant) { - this._mousedownOnUrlbarDescendant = false; - break; - } - // Don't close the view when clicking on a tab; we may want to keep the - // view open on tab switch, and the TabSelect event arrived earlier. - if (event.target.closest("tab")) { - break; - } - - // Close the view when clicking on toolbars and other UI pieces that - // might not automatically remove focus from the input. - // Respect the autohide preference for easier inspecting/debugging via - // the browser toolbox. - if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { - if (this.view.isOpen && !this.hasAttribute("focused")) { - // In this case, as blur event never happen from the inputField, we - // record abandonment event explicitly. - let blurEvent = new FocusEvent("blur", { - relatedTarget: this.inputField, - }); - this.controller.engagementEvent.record(blurEvent, { - searchString: this._lastSearchString, - searchSource: this.getSearchSource(blurEvent), - }); - } - - this.view.close(); - } - break; - } - } - - _on_input(event) { - if ( - this._autofillPlaceholder && - this.value === this.userTypedValue && - (event.inputType === "deleteContentBackward" || - event.inputType === "deleteContentForward") - ) { - // Take a telemetry if user deleted whole autofilled value. - Glean.urlbar.autofillDeletion.add(1); - } - - let value = this.value; - this.valueIsTyped = true; - this._untrimmedValue = value; - this._protocolIsTrimmed = false; - this._resultForCurrentValue = null; - - this.userTypedValue = value; - // Unset userSelectionBehavior because the user is modifying the search - // string, thus there's no valid selection. This is also used by the view - // to set "aria-activedescendant", thus it should never get stale. - this.controller.userSelectionBehavior = "none"; - - let compositionState = this.#compositionState; - let compositionClosedPopup = this.#compositionClosedPopup; - - // Clear composition values if we're no more composing. - if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) { - this.#compositionState = lazy.UrlbarUtils.COMPOSITION.NONE; - this.#compositionClosedPopup = false; - } - - this.toggleAttribute("usertyping", value); - this.removeAttribute("actiontype"); - - if ( - this.getAttribute("pageproxystate") == "valid" && - this.value != this._lastValidURLStr - ) { - this.setPageProxyState("invalid", true); - } - - if (this.#isAddressbar) { - let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); - if ( - state.persist?.shouldPersist && - this.value !== state.persist.searchTerms - ) { - state.persist.shouldPersist = false; - this.removeAttribute("persistsearchterms"); - } - } - - if (this.view.isOpen) { - if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) { - // SmartbarView rolls up all popups when it opens, but we should - // do the same for SmartbarInput when it's already open in case - // a tab preview was opened - this.window.docShell.treeOwner - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIAppWindow) - .rollupAllPopups(); - } - if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) { - this.view.clear(); - if (!this.searchMode || !this.view.oneOffSearchButtons?.hasView) { - this.view.close(); - return; - } - } - } else { - this.view.clear(); - } - - this.view.removeAccessibleFocus(); - - // During composition with an IME, the following events happen in order: - // 1. a compositionstart event - // 2. some input events - // 3. a compositionend event - // 4. an input event - - // We should do nothing during composition or if composition was canceled - // and we didn't close the popup on composition start. - if ( - !lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") && - (compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING || - (compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED && - !compositionClosedPopup)) - ) { - return; - } - - // Autofill only when text is inserted (i.e., event.data is not empty) and - // it's not due to pasting. - const allowAutofill = - (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") || - compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) && - !!event.data && - !lazy.UrlbarUtils.isPasteEvent(event) && - this._maybeAutofillPlaceholder(value); - - this.startQuery({ - searchString: value, - allowAutofill, - resetSearchState: false, - event, - }); - } - - _on_selectionchange() { - // Confirm placeholder as user text if it gets explicitly deselected. This - // happens when the user wants to modify the autofilled text by either - // clicking on it, or pressing HOME, END, RIGHT, … - if ( - this._autofillPlaceholder && - this._autofillPlaceholder.value == this.value && - (this._autofillPlaceholder.selectionStart != this.selectionStart || - this._autofillPlaceholder.selectionEnd != this.selectionEnd) - ) { - this._autofillPlaceholder = null; - this.userTypedValue = this.value; - } - } - - _on_select() { - // On certain user input, AutoCopyListener::OnSelectionChange() updates - // the primary selection with user-selected text (when supported). - // Selection::NotifySelectionListeners() then dispatches a "select" event - // under similar conditions via TextInputListener::OnSelectionChange(). - // This event is received here in order to replace the primary selection - // from the editor with text having the adjustments of - // _getSelectedValueForClipboard(), such as adding the scheme for the url. - // - // Other "select" events are also received, however, and must be excluded. - if ( - // _suppressPrimaryAdjustment is set during select(). Don't update - // the primary selection because that is not the intent of user input, - // which may be new tab or urlbar focus. - this._suppressPrimaryAdjustment || - // The check on isHandlingUserInput filters out async "select" events - // from setSelectionRange(), which occur when autofill text is selected. - !this.window.windowUtils.isHandlingUserInput || - !Services.clipboard.isClipboardTypeSupported( - Services.clipboard.kSelectionClipboard - ) - ) { - return; - } - - let val = this._getSelectedValueForClipboard(); - if (!val) { - return; - } - - lazy.ClipboardHelper.copyStringToClipboard( - val, - Services.clipboard.kSelectionClipboard - ); - } - - _on_overflow(event) { - const targetIsPlaceholder = - event.originalTarget.implementedPseudoElement == "::placeholder"; - // We only care about the non-placeholder text. - // This shouldn't be needed, see bug 1487036. - if (targetIsPlaceholder) { - return; - } - this._overflowing = true; - this.updateTextOverflow(); - } - - _on_underflow(event) { - const targetIsPlaceholder = - event.originalTarget.implementedPseudoElement == "::placeholder"; - // We only care about the non-placeholder text. - // This shouldn't be needed, see bug 1487036. - if (targetIsPlaceholder) { - return; - } - this._overflowing = false; - - this.updateTextOverflow(); - - this._updateUrlTooltip(); - } - - _on_paste(event) { - let originalPasteData = event.clipboardData.getData("text/plain"); - if (!originalPasteData) { - return; - } - - let oldValue = this.value; - let oldStart = oldValue.substring(0, this.selectionStart); - // If there is already non-whitespace content in the URL bar - // preceding the pasted content, it's not necessary to check - // protocols used by the pasted content: - if (oldStart.trim()) { - return; - } - let oldEnd = oldValue.substring(this.selectionEnd); - - const pasteData = this.sanitizeTextFromClipboard(originalPasteData); - - if (originalPasteData != pasteData) { - // Unfortunately we're not allowed to set the bits being pasted - // so cancel this event: - event.preventDefault(); - event.stopImmediatePropagation(); - - const value = oldStart + pasteData + oldEnd; - this._setValue(value, { valueIsTyped: true }); - this.userTypedValue = value; - - // Since we prevent the default paste event, we have to ensure the - // pageproxystate is updated. The paste event replaces the actual current - // page's URL with user-typed content, so we should set pageproxystate to - // invalid. - if (this.getAttribute("pageproxystate") == "valid") { - this.setPageProxyState("invalid"); - } - this.toggleAttribute("usertyping", this._untrimmedValue); - - // Fix up cursor/selection: - let newCursorPos = oldStart.length + pasteData.length; - this.setSelectionRange(newCursorPos, newCursorPos); - - this.startQuery({ - searchString: this.value, - allowAutofill: false, - resetSearchState: false, - event, - }); - } - } - - /** - * Sanitize and process data retrieved from the clipboard - * - * @param {string} clipboardData - * The original data retrieved from the clipboard. - * @returns {string} - * The sanitized paste data, ready to use. - */ - sanitizeTextFromClipboard(clipboardData) { - let fixedURI, keywordAsSent; - try { - ({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo( - clipboardData, - Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | - Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP - )); - } catch (e) {} - - let pasteData; - if (keywordAsSent) { - // For performance reasons, we don't want to beautify a long string. - if (clipboardData.length < 500) { - // For only keywords, replace any white spaces including line break - // with white space. - pasteData = clipboardData.replace(/\s/g, " "); - } else { - pasteData = clipboardData; - } - } else if ( - fixedURI?.scheme == "data" && - !fixedURI.spec.match(/^data:.+;base64,/) - ) { - // For data url without base64, replace line break with white space. - pasteData = clipboardData.replace(/[\r\n]/g, " "); - } else { - // For normal url or data url having basic64, or if fixup failed, just - // remove line breaks. - pasteData = clipboardData.replace(/[\r\n]/g, ""); - } - - return lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData); - } - - /** - * Generate a UrlbarQueryContext from the current context. - * - * @param {object} [options] - * Optional params - * @param {boolean} [options.allowAutofill] - * Whether autofill is enabled. - * @param {string} [options.searchString] - * The string being searched. - * @param {object} [options.event] - * The event triggering the query. - * @returns {UrlbarQueryContext} - * The queryContext object. - */ - #makeQueryContext({ - allowAutofill = true, - searchString = null, - event = null, - } = {}) { - // When we are in actions search mode we can show more results so - // increase the limit. - let maxResults = - this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS - ? lazy.UrlbarPrefs.get("maxRichResults") - : UNLIMITED_MAX_RESULTS; - let options = { - allowAutofill, - isPrivate: this.isPrivate, - sapName: this.sapName, - maxResults, - searchString, - prohibitRemoteResults: - event && - lazy.UrlbarUtils.isPasteEvent(event) && - lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") < - event.data?.length, - }; - - // Only add gBrowser-dependent properties if gBrowser exists. - if (this.window.gBrowser) { - options.userContextId = parseInt( - this.window.gBrowser.selectedBrowser?.getAttribute("usercontextid") ?? 0 - ); - options.tabGroup = this.window.gBrowser.selectedTab.group?.id ?? null; - options.currentPage = this.window.gBrowser.currentURI?.spec ?? ""; - } - - if (this.searchMode) { - options.searchMode = this.searchMode; - if (this.searchMode.source) { - options.sources = [this.searchMode.source]; - } - } - - return new lazy.UrlbarQueryContext(options); - } - - _on_scrollend() { - this.updateTextOverflow(); - } - - _on_TabSelect() { - // TabSelect may be activated by a keyboard shortcut and cause the urlbar - // to take focus, in this case we should not untrim. - this._untrimOnFocusAfterKeydown = false; - this._gotTabSelect = true; - this._afterTabSelectAndFocusChange(); - } - - _on_TabClose(event) { - this.controller.engagementEvent.handleBounceEventTrigger( - event.target.linkedBrowser - ); - - if (this.view.isOpen) { - // Refresh results when a tab is closed while the results view is open. - // This prevents switch-to-tab results from remaining in the results - // list after their tab is closed. - this.startQuery(); - } - } - - _on_beforeinput(event) { - if (event.data && this._keyDownEnterDeferred) { - // Ignore char key input while processing enter key. - event.preventDefault(); - } - } - - _on_keydown(event) { - if (event.currentTarget == this.window) { - // It would be great if we could more easily detect the user focusing the - // address bar through a keyboard shortcut, but F6 and TAB bypass are - // not going through commands handling. - // Also note we'll unset this on TabSelect, as it can focus the address - // bar but we should not untrim in that case. - this._untrimOnFocusAfterKeydown = !this.focused; - return; - } - - if ( - this.#isSmartbarMode && - event.keyCode === KeyEvent.DOM_VK_RETURN && - event.shiftKey - ) { - event.preventDefault(); - return; - } - - if (!this.controller) { - return; - } - - // Repeated KeyboardEvents can easily cause subtle bugs in this logic, if - // not properly handled, so let's first handle things that should not be - // evaluated repeatedly. - if (!event.repeat) { - this.#allTextSelectedOnKeyDown = this.#allTextSelected; - - if (event.keyCode === KeyEvent.DOM_VK_RETURN) { - if (this._keyDownEnterDeferred) { - this._keyDownEnterDeferred.reject(); - } - this._keyDownEnterDeferred = Promise.withResolvers(); - event._disableCanonization = - AppConstants.platform == "macosx" - ? this._isKeyDownWithMeta - : this._isKeyDownWithCtrl; - } - - // Now set the keydown trackers for the current event, anything that wants - // to check the previous events should have happened before this point. - // The previously value is persisted until keyup, as we check if the - // modifiers were down, even if other keys are pressed in the meanwhile. - if (event.ctrlKey && event.keyCode != KeyEvent.DOM_VK_CONTROL) { - this._isKeyDownWithCtrl = true; - } - if (event.metaKey && event.keyCode != KeyEvent.DOM_VK_META) { - this._isKeyDownWithMeta = true; - } - // This is used in keyup, so it can be set every time. - this._isKeyDownWithMetaAndLeft = - this._isKeyDownWithMeta && - !event.shiftKey && - event.keyCode == KeyEvent.DOM_VK_LEFT; - - this._toggleActionOverride(event); - } - - // Due to event deferring, it's possible preventDefault() won't be invoked - // soon enough to actually prevent some of the default behaviors, thus we - // have to handle the event "twice". This first immediate call passes false - // as second argument so that handleKeyNavigation will only simulate the - // event handling, without actually executing actions. - // TODO (Bug 1541806): improve this handling, maybe by delaying actions - // instead of events. - if (this.eventBufferer.shouldDeferEvent(event)) { - this.controller.handleKeyNavigation(event, false); - } - this.eventBufferer.maybeDeferEvent(event, () => { - this.controller.handleKeyNavigation(event); - }); - } - - async _on_keyup(event) { - if (event.currentTarget == this.window) { - this._untrimOnFocusAfterKeydown = false; - return; - } - - if (this.#allTextSelectedOnKeyDown) { - let moveCursorToStart = this.#isHomeKeyUpEvent(event); - // We must set the selection immediately because: - // - on Mac Fn + Left is not handled properly as Home - // - untrim depends on text not being fully selected. - if (moveCursorToStart) { - this.selectionStart = this.selectionEnd = 0; - } - this.#maybeUntrimUrl({ moveCursorToStart }); - } - if (event.keyCode === KeyEvent.DOM_VK_META) { - this._isKeyDownWithMeta = false; - this._isKeyDownWithMetaAndLeft = false; - } - if (event.keyCode === KeyEvent.DOM_VK_CONTROL) { - this._isKeyDownWithCtrl = false; - } - - this._toggleActionOverride(event); - - // Pressing Enter key while pressing Meta key, and next, even when releasing - // Enter key before releasing Meta key, the keyup event is not fired. - // Therefore, if Enter keydown is detecting, continue the post processing - // for Enter key when any keyup event is detected. - if (this._keyDownEnterDeferred) { - if (this._keyDownEnterDeferred.loadedContent) { - try { - const loadingBrowser = await this._keyDownEnterDeferred.promise; - // Ensure the selected browser didn't change in the meanwhile. - if (this.window.gBrowser.selectedBrowser === loadingBrowser) { - loadingBrowser.focus(); - // Make sure the domain name stays visible for spoof protection and usability. - this.setSelectionRange(0, 0); - } - } catch (ex) { - // Not all the Enter actions in the urlbar will cause a navigation, then it - // is normal for this to be rejected. - // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here - // to ensure not overwriting the new value created by keydown. - } - } else { - // Discard the _keyDownEnterDeferred promise to receive any key inputs immediately. - this._keyDownEnterDeferred.resolve(); - } - - this._keyDownEnterDeferred = null; - } - } - - _on_compositionstart() { - if (this.#compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) { - throw new Error("Trying to start a nested composition?"); - } - this.#compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING; - - if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) { - return; - } - - // Close the view. This will also stop searching. - if (this.view.isOpen) { - // We're closing the view, but we want to retain search mode if the - // selected result was previewing it. - if (this.searchMode) { - // If we entered search mode with an empty string, clear userTypedValue, - // otherwise confirmSearchMode may try to set it as value. - // This can happen for example if we entered search mode typing a - // a partial engine domain and selecting a tab-to-search result. - if (!this.value) { - this.userTypedValue = null; - } - this.confirmSearchMode(); - } - this.#compositionClosedPopup = true; - this.view.close(); - } else { - this.#compositionClosedPopup = false; - } - } - - _on_compositionend(event) { - if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) { - throw new Error("Trying to stop a non existing composition?"); - } - - if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) { - // Clear the selection and the cached result, since they refer to the - // state before this composition. A new input even will be generated - // after this. - this.view.clearSelection(); - this._resultForCurrentValue = null; - } - - // We can't yet retrieve the committed value from the editor, since it isn't - // completely committed yet. We'll handle it at the next input event. - this.#compositionState = event.data - ? lazy.UrlbarUtils.COMPOSITION.COMMIT - : lazy.UrlbarUtils.COMPOSITION.CANCELED; - } - - _on_dragstart(event) { - // Drag only if the gesture starts from the input field. - let nodePosition = this.inputField.compareDocumentPosition( - event.originalTarget - ); - if ( - event.target != this.inputField && - !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY) - ) { - return; - } - - // Don't cover potential drop targets on the toolbars or in content. - this.view.close(); - - // Only customize the drag data if the entire value is selected and it's a - // loaded URI. Use default behavior otherwise. - if ( - !this.#allTextSelected || - this.getAttribute("pageproxystate") != "valid" - ) { - return; - } - - let uri = this.makeURIReadable(this.window.gBrowser.currentURI); - let href = uri.displaySpec; - let title = this.window.gBrowser.contentTitle || href; - - event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`); - event.dataTransfer.setData("text/plain", href); - event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`); - event.dataTransfer.effectAllowed = "copyLink"; - event.stopPropagation(); - } - - /** - * Handles dragover events for the input. - * - * @param {DragEvent} event - */ - _on_dragover(event) { - if (!getDroppableData(event)) { - event.dataTransfer.dropEffect = "none"; - } - } - - /** - * Handles dropping of data on the input. - * - * @param {DragEvent} event - */ - _on_drop(event) { - let droppedItem = getDroppableData(event); - let droppedURL = URL.isInstance(droppedItem) - ? droppedItem.href - : droppedItem; - if (droppedURL && droppedURL !== this.window.gBrowser.currentURI.spec) { - let principal = Services.droppedLinkHandler.getTriggeringPrincipal(event); - this.value = droppedURL; - this.setPageProxyState("invalid"); - this.focus(); - // To simplify tracking of events, register an initial event for event - // telemetry, to replace the missing input event. - let queryContext = this.#makeQueryContext({ searchString: droppedURL }); - this.controller.setLastQueryContextCache(queryContext); - this.controller.engagementEvent.start(event, queryContext); - this.handleNavigation({ triggeringPrincipal: principal }); - if (this.#isAddressbar) { - // For safety reasons, in the drop case we don't want to immediately show - // the dropped value, instead we want to keep showing the current page - // url until an onLocationChange happens. - // See the handling in `setURI` for further details. - this.userTypedValue = null; - this.setURI({ dueToTabSwitch: true }); - } - } - } - - _on_customizationstarting() { - this.incrementBreakoutBlockerCount(); - this.blur(); - } - - _on_aftercustomization() { - this.decrementBreakoutBlockerCount(); - this.#updateLayoutBreakout(); - } - - uiDensityChanged() { - if (this.#breakoutBlockerCount) { - return; - } - this.#updateLayoutBreakout(); - } - - _on_toolbarvisibilitychange() { - this.#updateTextboxPositionNextFrame(); - } - - _on_DOMMenuBarActive() { - this.#updateTextboxPositionNextFrame(); - } - - _on_DOMMenuBarInactive() { - this.#updateTextboxPositionNextFrame(); - } - - #allTextSelectedOnKeyDown = false; - get #allTextSelected() { - return this.selectionStart == 0 && this.selectionEnd == this.value.length; - } - - /** - * @param {string} value - * A untrimmed address bar input. - * @returns {nsILoadInfo.SchemelessInputType} - * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeless` if the input - * doesn't start with a scheme relevant for schemeless HTTPS-First - * (http://, https:// and file://). - * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeful` if it does have a scheme. - */ - #getSchemelessInput(value) { - return ["http://", "https://", "file://"].every( - scheme => !value.trim().startsWith(scheme) - ) - ? Ci.nsILoadInfo.SchemelessInputTypeSchemeless - : Ci.nsILoadInfo.SchemelessInputTypeSchemeful; - } - - get #isOpenedPageInBlankTargetLoading() { - return ( - this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory - ?.count === 0 && - this.window.gBrowser.selectedBrowser.browsingContext - .nonWebControlledBlankURI - ); - } - - // Search modes are per browser and are stored in the `searchModes` property of this map. - // For a browser, search mode can be in preview mode, confirmed, or both. - // Typically, search mode is entered in preview mode with a particular - // source and is confirmed with the same source once a query starts. It's - // also possible for a confirmed search mode to be replaced with a preview - // mode with a different source, and in those cases, we need to re-confirm - // search mode when preview mode is exited. In addition, only confirmed - // search modes should be restored across sessions. We therefore need to - // keep track of both the current confirmed and preview modes, per browser. - // - // For each browser with a search mode, this maps the browser to an object - // like this: { preview, confirmed }. Both `preview` and `confirmed` are - // search mode objects; see the setSearchMode documentation. Either one may - // be undefined if that particular mode is not active for the browser. - - /** - * Tracks a state object per browser. - */ - #browserStates = new WeakMap(); - - get #selectedText() { - return this.editor.selection.toStringWithFormat( - "text/plain", - Ci.nsIDocumentEncoder.OutputPreformatted | - Ci.nsIDocumentEncoder.OutputRaw, - 0 - ); - } - - /** - * Check whether a key event has a similar effect as the Home key. - * - * @param {KeyboardEvent} event A Keyboard event - * @returns {boolean} Whether the even will act like the Home key. - */ - #isHomeKeyUpEvent(event) { - let isMac = AppConstants.platform === "macosx"; - return ( - // On MacOS this can be generated with Fn + Left. - event.keyCode == KeyEvent.DOM_VK_HOME || - // Windows and Linux also support Ctrl + Left. - (!isMac && - event.keyCode == KeyboardEvent.DOM_VK_LEFT && - event.ctrlKey && - !event.shiftKey) || - // MacOS supports other combos to move cursor at the start of the line. - // For example Ctrl + A. - (isMac && - event.keyCode == KeyboardEvent.DOM_VK_A && - event.ctrlKey && - !event.shiftKey) || - // And also Cmd (Meta) + Left. - // Unfortunately on MacOS it's not possible to detect combos with the meta - // key during the keyup event, due to how the OS handles events. Thus we - // record the combo on keydown, and check for it here. - (isMac && - event.keyCode == KeyEvent.DOM_VK_META && - this._isKeyDownWithMetaAndLeft) - ); - } -} - -/** - * Tries to extract droppable data from a DND event. - * - * @param {DragEvent} event The DND event to examine. - * @returns {URL|string|null} - * null if there's a security reason for which we should do nothing. - * A URL object if it's a value we can load. - * A string value otherwise. - */ -function getDroppableData(event) { - let links; - try { - links = Services.droppedLinkHandler.dropLinks(event); - } catch (ex) { - // This is either an unexpected failure or a security exception; in either - // case we should always return null. - return null; - } - // The URL bar automatically handles inputs with newline characters, - // so we can get away with treating text/x-moz-url flavours as text/plain. - if (links[0]?.url) { - event.preventDefault(); - let href = links[0].url; - if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) { - // We may have stripped an unsafe protocol like javascript: and if so - // there's no point in handling a partial drop. - event.stopImmediatePropagation(); - return null; - } - - // If this fails, checkLoadURIStrWithPrincipal would also fail, - // as that's what it does with things that don't pass the IO - // service's newURI constructor without fixup. It's conceivable we - // may want to relax this check in the future (so e.g. www.foo.com - // gets fixed up), but not right now. - let url = URL.parse(href); - if (url) { - // If we succeed, try to pass security checks. If this works, return the - // URL object. If the *security checks* fail, return null. - try { - let principal = - Services.droppedLinkHandler.getTriggeringPrincipal(event); - Services.scriptSecurityManager.checkLoadURIStrWithPrincipal( - principal, - url.href, - Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL - ); - return url; - } catch (ex) { - return null; - } - } - // We couldn't make a URL out of this. Continue on, and return text below. - } - // Handle as text. - return event.dataTransfer.getData("text/plain"); -} - -/** - * Decodes the given URI for displaying it in the address bar without losing - * information, such that hitting Enter again will load the same URI. - * - * @param {nsIURI} aURI - * The URI to decode - * @returns {string} - * The decoded URI - */ -function losslessDecodeURI(aURI) { - let scheme = aURI.scheme; - let value = aURI.displaySpec; - - // Try to decode as UTF-8 if there's no encoding sequence that we would break. - if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) { - let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme); - if (decodeASCIIOnly) { - // This only decodes ascii characters (hex) 20-7e, except 25 (%). - // This avoids both cases stipulated below (%-related issues, and \r, \n - // and \t, which would be %0d, %0a and %09, respectively) as well as any - // non-US-ascii characters. - value = value.replace( - /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, - decodeURI - ); - } else { - try { - value = decodeURI(value) - // decodeURI decodes %25 to %, which creates unintended encoding - // sequences. Re-encode it, unless it's part of a sequence that - // survived decodeURI, i.e. one for: - // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#' - // (RFC 3987 section 3.2) - .replace( - /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi, - encodeURIComponent - ); - } catch (e) {} - } - } - - // IMPORTANT: The following regular expressions are Unicode-aware due to /v. - // Avoid matching high or low surrogate pairs directly, always work with - // full Unicode scalar values. - - // Encode potentially invisible characters: - // U+0000-001F: C0/C1 control characters - // U+007F-009F: commands - // U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces - // U+2028-2029: line and paragraph separators - // U+2800: braille empty pattern - // U+FFFC: object replacement character - // Encode any trailing whitespace that may be part of a pasted URL, so that it - // doesn't get eaten away by the location bar (bug 410726). - // Encode all adjacent space chars (U+0020), to prevent spoofing attempts - // where they would push part of the URL to overflow the location bar - // (bug 1395508). A single space, or the last space if the are many, is - // preserved to maintain readability of certain urls if it's not followed by a - // control or separator character. We only do this for the common space, - // because others may be eaten when copied to the clipboard,so it's safer to - // preserve them encoded. - value = value.replace( - // eslint-disable-next-line no-control-regex - /[[\p{Separator}--\u{0020}]\p{Control}\u{2800}\u{FFFC}]|\u{0020}(?=[\p{Other}\p{Separator}])|\s$/gv, - encodeURIComponent - ); - - // Encode characters that are ignorable, can't be rendered usefully, or may - // confuse users. - // - // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded - // per bug 582186: - // U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E, - // U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0, - // U+FFF0-FFFB, U+1D173-1D17A, U+E0000-E0FFF - // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6): - // U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069 - // Other format characters in the Cf category that are unlikely to be rendered - // usefully: - // U+0600-0605, U+08E2, U+110BD, U+110CD, U+13430-13438, U+1BCA0-1BCA3 - // Mimicking UI parts: - // U+1F50F-1F513, U+1F6E1 - // Unassigned codepoints, sometimes shown as empty glyphs. - value = value.replace( - // eslint-disable-next-line no-misleading-character-class - /[[\p{Format}--[\u{200C}\u{200D}]]\u{034F}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180B}-\u{180D}\u{3164}\u{FE00}-\u{FE0F}\u{FFA0}\u{FFF0}-\u{FFFB}\p{Unassigned}\p{Private_Use}\u{E0000}-\u{E0FFF}\u{1F50F}-\u{1F513}\u{1F6E1}]/gv, - encodeURIComponent - ); - return value; -} - -/** - * Handles copy and cut commands for the urlbar. - */ -class CopyCutController { - /** - * @param {SmartbarInput} urlbar - * The SmartbarInput instance to use this controller for. - */ - constructor(urlbar) { - this.urlbar = urlbar; - } - - /** - * @param {string} command - * The name of the command to handle. - */ - doCommand(command) { - let urlbar = this.urlbar; - let val = urlbar._getSelectedValueForClipboard(); - if (!val) { - return; - } - - if (command == "cmd_cut" && this.isCommandEnabled(command)) { - let start = urlbar.selectionStart; - let end = urlbar.selectionEnd; - urlbar.inputField.value = - urlbar.inputField.value.substring(0, start) + - urlbar.inputField.value.substring(end); - urlbar.inputField.setSelectionRange(start, start); - - let event = new UIEvent("input", { - bubbles: true, - cancelable: false, - view: urlbar.window, - detail: 0, - }); - urlbar.inputField.dispatchEvent(event); - } - - lazy.ClipboardHelper.copyString(val); - } - - /** - * @param {string} command - * The name of the command to check. - * @returns {boolean} - * Whether the command is handled by this controller. - */ - supportsCommand(command) { - switch (command) { - case "cmd_copy": - case "cmd_cut": - return true; - } - return false; - } - - /** - * @param {string} command - * The name of the command to check. - * @returns {boolean} - * Whether the command should be enabled. - */ - isCommandEnabled(command) { - return ( - this.supportsCommand(command) && - (command != "cmd_cut" || !this.urlbar.readOnly) && - this.urlbar.selectionStart < this.urlbar.selectionEnd - ); - } - - onEvent() {} -} - -/** - * Manages the Add Search Engine contextual menu entries. - * - * Note: setEnginesFromBrowser must be invoked from the outside when the - * page provided engines list changes. - * refreshContextMenu must be invoked when the context menu is opened. - */ -class AddSearchEngineHelper { - /** - * @type {UrlbarSearchOneOffs} - */ - shortcutButtons; - - /** - * @param {SmartbarInput} input The parent SmartbarInput. - */ - constructor(input) { - this.input = input; - this.shortcutButtons = input.view.oneOffSearchButtons; - } - - /** - * If there's more than this number of engines, the context menu offers - * them in a submenu. - * - * @returns {number} - */ - get maxInlineEngines() { - return this.shortcutButtons._maxInlineAddEngines; - } - - /** - * Invoked by OpenSearchManager when the list of available engines changes. - * - * @param {object} browser The current browser. - * @param {object} engines The updated list of available engines. - */ - setEnginesFromBrowser(browser, engines) { - this.browsingContext = browser.browsingContext; - // Make a copy of the array for state comparison. - engines = engines.slice(); - if (!this._sameEngines(this.engines, engines)) { - this.engines = engines; - this.shortcutButtons?.updateWebEngines(); - } - } - - _sameEngines(engines1, engines2) { - if (engines1?.length != engines2?.length) { - return false; - } - return lazy.ObjectUtils.deepEqual( - engines1.map(e => e.title), - engines2.map(e => e.title) - ); - } - - _createMenuitem(engine, index) { - let elt = this.input.document.createXULElement("menuitem"); - elt.setAttribute("anonid", `add-engine-${index}`); - elt.classList.add("menuitem-iconic"); - elt.classList.add("context-menu-add-engine"); - this.input.document.l10n.setAttributes(elt, "search-one-offs-add-engine", { - engineName: engine.title, - }); - elt.setAttribute("uri", engine.uri); - if (engine.icon) { - elt.setAttribute("image", engine.icon); - } else { - elt.removeAttribute("image"); - } - elt.addEventListener("command", this._onCommand.bind(this)); - return elt; - } - - _createMenu(engine) { - let elt = this.input.document.createXULElement("menu"); - elt.setAttribute("anonid", "add-engine-menu"); - elt.classList.add("menu-iconic"); - elt.classList.add("context-menu-add-engine"); - this.input.document.l10n.setAttributes( - elt, - "search-one-offs-add-engine-menu" - ); - if (engine.icon) { - elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(engine.icon)); - } - let popup = this.input.document.createXULElement("menupopup"); - elt.appendChild(popup); - return elt; - } - - refreshContextMenu() { - let engines = this.engines; - let contextMenu = this.input.querySelector("moz-input-box").menupopup; - - // Certain operations, like customization, destroy and recreate widgets, - // so we cannot rely on cached elements. - if (!contextMenu.querySelector(".menuseparator-add-engine")) { - this.contextSeparator = - this.input.document.createXULElement("menuseparator"); - this.contextSeparator.setAttribute("anonid", "add-engine-separator"); - this.contextSeparator.classList.add("menuseparator-add-engine"); - this.contextSeparator.collapsed = true; - contextMenu.appendChild(this.contextSeparator); - } - - this.contextSeparator.collapsed = !engines.length; - let curElt = this.contextSeparator; - // Remove the previous items, if any. - for (let elt = curElt.nextElementSibling; elt; ) { - let nextElementSibling = elt.nextElementSibling; - elt.remove(); - elt = nextElementSibling; - } - - // If the page provides too many engines, we only show a single menu entry - // with engines in a submenu. - if (engines.length > this.maxInlineEngines) { - // Set the menu button's image to the image of the first engine. The - // offered engines may have differing images, so there's no perfect - // choice here. - let elt = this._createMenu(engines[0]); - this.contextSeparator.insertAdjacentElement("afterend", elt); - curElt = elt.lastElementChild; - } - - // Insert the engines, either in the contextual menu or the sub menu. - for (let i = 0; i < engines.length; ++i) { - let elt = this._createMenuitem(engines[i], i); - if (curElt.localName == "menupopup") { - curElt.appendChild(elt); - } else { - curElt.insertAdjacentElement("afterend", elt); - } - curElt = elt; - } - } - - async _onCommand(event) { - let added = await lazy.SearchUIUtils.addOpenSearchEngine( - event.target.getAttribute("uri"), - event.target.getAttribute("image"), - this.browsingContext - ).catch(console.error); - if (added) { - // Remove the offered engine from the list. The browser updated the - // engines list at this point, so we just have to refresh the menu.) - this.refreshContextMenu(); - } - } -} - -customElements.define("moz-smartbar", SmartbarInput); diff --git a/browser/components/urlbar/content/SmartbarInputController.mjs b/browser/components/urlbar/content/SmartbarInputController.mjs @@ -1,216 +0,0 @@ -/* 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/. */ - -/** - * @import {MultilineEditor} from "chrome://browser/content/multilineeditor/multiline-editor.mjs" - */ - -/** - * Controller for the Smartbar editor. - * - * The controller abstracts the underlying editor implementation allowing - * the Smartbar to also work with a standard HTML input. - */ -export class SmartbarInputController { - /** - * @param {object} adapter - * Adapter with input and editor references. - * @param {MultilineEditor | HTMLInputElement} adapter.input - * The input element. - * @param {object} adapter.editor - * The editor object. - */ - constructor(adapter) { - /** @type {MultilineEditor | HTMLInputElement} */ - this.input = adapter.input; - /** @type {object} */ - this.editor = adapter.editor; - } - - /** - * Focuses the input element. - */ - focus() { - this.input.focus(); - } - - /** - * Removes focus from the input element. - */ - blur() { - this.input.blur(); - } - - /** - * Whether the input is read-only. - * - * @type {boolean} - */ - get readOnly() { - return this.input.readOnly; - } - - /** - * Sets the read-only state of the input. - * - * @param {boolean} val - */ - set readOnly(val) { - this.input.readOnly = val; - } - - /** - * The placeholder text for the input. - * - * @type {string} - */ - get placeholder() { - return this.input.placeholder ?? ""; - } - - /** - * Sets the placeholder text for the input. - * - * @param {string} val - */ - set placeholder(val) { - this.input.placeholder = val ?? ""; - } - - /** - * The current value of the input. - * - * @type {string} - */ - get value() { - return this.input.value ?? ""; - } - - /** - * Sets the value of the input. - * - * @param {string} val - */ - setValue(val) { - this.input.value = val ?? ""; - } - - /** - * The start offset of the selection. - * - * @type {number} - */ - get selectionStart() { - return this.input.selectionStart ?? 0; - } - - /** - * Sets the start offset of the selection. - * - * @param {number} val - */ - set selectionStart(val) { - this.setSelectionRange(val, this.selectionEnd ?? val); - } - - /** - * The end offset of the selection. - * - * @type {number} - */ - get selectionEnd() { - return this.input.selectionEnd ?? 0; - } - - /** - * Sets the end offset of the selection. - * - * @param {number} val - */ - set selectionEnd(val) { - this.setSelectionRange(this.selectionStart ?? 0, val); - } - - /** - * Sets the selection range in the input. - * - * @param {number} start - * The start offset. - * @param {number} end - * The end offset. - */ - setSelectionRange(start, end) { - if (!this.input.setSelectionRange) { - return; - } - const from = Math.max(0, start ?? 0); - const to = Math.max(from, end ?? from); - this.input.setSelectionRange(from, to); - } - - /** - * Selects all text in the input. - */ - select() { - this.input.select?.(); - } - - /** - * Dispatches an input event on the input element. - * - * @param {object} [options] - * The event options. - * @param {string} [options.inputType="insertText"] - * The input type. - * @param {string} [options.data=""] - * The data being inserted. - */ - dispatchInput({ inputType = "insertText", data = "" } = {}) { - this.input.dispatchEvent( - new InputEvent("input", { - bubbles: true, - composed: true, - inputType, - data, - }) - ); - } - - /** - * Dispatches a selectionchange event on the input element. - */ - dispatchSelectionChange() { - this.input.dispatchEvent( - new Event("selectionchange", { bubbles: true, composed: true }) - ); - } - - /** - * Whether the editor is in composition mode. - * - * @type {boolean} - */ - get composing() { - return this.editor.composing ?? false; - } - - /** - * The number of selection ranges in the editor. - * - * @type {number} - */ - get selectionRangeCount() { - return this.editor.selection?.rangeCount ?? 0; - } - - /** - * Returns the string representation of the current selection with formatting. - * - * @returns {string} - * The formatted selection string. - */ - selectionToStringWithFormat() { - return this.editor.selection?.toStringWithFormat() ?? ""; - } -} diff --git a/browser/components/urlbar/content/SmartbarInputUtils.mjs b/browser/components/urlbar/content/SmartbarInputUtils.mjs @@ -1,88 +0,0 @@ -/* 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/. */ - -import { MultilineEditor } from "chrome://browser/content/multilineeditor/multiline-editor.mjs"; - -/** - * Creates a Smartbar editor element. - * - * @param {HTMLInputElement | MultilineEditor} inputElement - * The input element to replace. - * @returns {{ - * input: MultilineEditor, - * editor: object - * } | null} - * An object with the new editor element and the adapter. - */ -export function createEditor(inputElement) { - if (!inputElement) { - return null; - } - - if (inputElement instanceof MultilineEditor) { - return { - input: inputElement, - editor: createEditorAdapter(inputElement), - }; - } - - const doc = inputElement.ownerDocument; - const editorElement = /** @type {MultilineEditor} */ ( - doc.createElement("moz-multiline-editor") - ); - - // Copy attributes except those that don’t apply. - for (const attr of inputElement.attributes) { - if (attr.name == "type" || attr.name == "value") { - continue; - } - editorElement.setAttribute(attr.name, attr.value); - } - - editorElement.className = inputElement.className; - editorElement.id = inputElement.id; - editorElement.value = inputElement.value ?? ""; - - inputElement.replaceWith(editorElement); - return { - input: editorElement, - editor: createEditorAdapter(editorElement), - }; -} - -/** - * Creates an adapter for the Smartbar editor element. - * - * @param {MultilineEditor} editorElement - * The editor element. - */ -export function createEditorAdapter(editorElement) { - const getSelectionBounds = () => { - let start = editorElement.selectionStart ?? 0; - let end = editorElement.selectionEnd ?? start; - if (start > end) { - [start, end] = [end, start]; - } - return { start, end }; - }; - - return { - get composing() { - return !!editorElement.composing; - }, - selection: { - get rangeCount() { - const { start, end } = getSelectionBounds(); - return start === end && editorElement.value === "" ? 0 : 1; - }, - toStringWithFormat() { - const { start, end } = getSelectionBounds(); - if (start == null || end == null) { - return ""; - } - return editorElement.value?.substring(start, end); - }, - }, - }; -} diff --git a/browser/components/urlbar/jar.mn b/browser/components/urlbar/jar.mn @@ -3,7 +3,4 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. browser.jar: - content/browser/urlbar/SmartbarInputController.mjs (content/SmartbarInputController.mjs) - content/browser/urlbar/SmartbarInputUtils.mjs (content/SmartbarInputUtils.mjs) - content/browser/urlbar/SmartbarInput.mjs (content/SmartbarInput.mjs) - content/browser/urlbar/UrlbarInput.mjs (content/UrlbarInput.mjs) + content/browser/urlbar/UrlbarInput.mjs (content/UrlbarInput.mjs) diff --git a/browser/components/urlbar/tsconfig.json b/browser/components/urlbar/tsconfig.json @@ -1,7 +1,6 @@ { "include": ["**/*.mjs", "types/*.ts"], "exclude": [ - "content/SmartbarInput.mjs", "content/UrlbarInput.mjs", "UrlbarProviderGlobalActions.sys.mjs", "UrlbarView.sys.mjs", diff --git a/browser/locales-preview/aiWindow.ftl b/browser/locales-preview/aiWindow.ftl @@ -28,11 +28,6 @@ aiwindow-input-cta-label-chat = Chat aiwindow-input-cta-label-search = Search aiwindow-input-cta-label-navigate = Navigate -## Smartbar - -smartbar-placeholder = - .placeholder = Ask, search, or type a URL - ## Firstrun onboarding aiwindow-firstrun-title = Welcome to Smart Window diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css @@ -24,7 +24,6 @@ @import url("chrome://browser/skin/places/editBookmarkPanel.css"); @import url("chrome://browser/skin/searchbar.css"); @import url("chrome://browser/skin/sidebar.css"); -@import url("chrome://browser/skin/smartbar.css"); @import url("chrome://browser/skin/aiWindowSidebar.css"); @import url("chrome://browser/skin/customizableui/customizeMode.css"); @import url("chrome://browser/skin/UITour.css"); diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn @@ -27,7 +27,6 @@ skin/classic/browser/sanitizeDialog_v2.css (../shared/sanitizeDialog_v2.css) skin/classic/browser/setDesktopBackground.css (../shared/setDesktopBackground.css) skin/classic/browser/sidebar.css (../shared/sidebar.css) - skin/classic/browser/smartbar.css (../shared/smartbar.css) skin/classic/browser/toolbarbuttons.css (../shared/toolbarbuttons.css) skin/classic/browser/toolbarbutton-icons.css (../shared/toolbarbutton-icons.css) skin/classic/browser/urlbar/flight-airline.svg (../shared/urlbar/flight-airline.svg) diff --git a/browser/themes/shared/smartbar.css b/browser/themes/shared/smartbar.css @@ -1,25 +0,0 @@ -/* 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/. */ - -.smartbar { - --smartbar-min-height: var(--urlbar-min-height); - bottom: 0; - display: block; - height: auto; - min-height: var(--smartbar-min-height); - position: absolute; - width: 100%; -} - -.smartbar .urlbar-input-box { - align-items: stretch; -} - -.smartbar .urlbar-input { - box-sizing: border-box; - height: auto; - min-height: var(--smartbar-min-height); - white-space: pre-wrap; - width: 100%; -} diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js @@ -171,7 +171,6 @@ module.exports = [ "browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css", "browser/themes/shared/search/searchbar.css", "browser/themes/shared/sidebar.css", - "browser/themes/shared/smartbar.css", "browser/themes/shared/syncedtabs/sidebar.css", "browser/themes/shared/tab-list-tree.css", "browser/themes/shared/tabbrowser/content-area.css", diff --git a/third_party/js/prosemirror/moz.build b/third_party/js/prosemirror/moz.build @@ -8,6 +8,5 @@ with Files("**"): BUG_COMPONENT = ("Firefox", "General") MOZ_SRC_FILES += [ - "prosemirror-view/style/prosemirror.css", "prosemirror.bundle.mjs", ] diff --git a/tools/@types/generated/lib.gecko.modules.d.ts b/tools/@types/generated/lib.gecko.modules.d.ts @@ -19,13 +19,11 @@ export interface Modules { "chrome://browser/content/sidebar/sidebar-panel-header.mjs": typeof import("chrome://browser/content/sidebar/sidebar-panel-header.mjs"), "chrome://browser/content/tabbrowser/tab-hover-preview.mjs": typeof import("chrome://browser/content/tabbrowser/tab-hover-preview.mjs"), "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs": typeof import("chrome://browser/content/translations/TranslationsPanelShared.sys.mjs"), - "chrome://browser/content/urlbar/SmartbarInput.mjs": typeof import("chrome://browser/content/urlbar/SmartbarInput.mjs"), "chrome://browser/content/urlbar/UrlbarInput.mjs": typeof import("chrome://browser/content/urlbar/UrlbarInput.mjs"), "chrome://browser/content/webrtc/webrtc-preview.mjs": typeof import("chrome://browser/content/webrtc/webrtc-preview.mjs"), "chrome://devtools-startup/content/DevToolsShim.sys.mjs": typeof import("chrome://devtools-startup/content/DevToolsShim.sys.mjs"), "chrome://formautofill/content/manageDialog.mjs": typeof import("chrome://formautofill/content/manageDialog.mjs"), "chrome://global/content/aboutLogging/profileStorage.mjs": typeof import("chrome://global/content/aboutLogging/profileStorage.mjs"), - "chrome://global/content/bindings/colorpicker-common.mjs": typeof import("chrome://global/content/bindings/colorpicker-common.mjs"), "chrome://global/content/certviewer/certDecoder.mjs": typeof import("chrome://global/content/certviewer/certDecoder.mjs"), "chrome://global/content/elements/browser-custom-element.mjs": typeof import("chrome://global/content/elements/browser-custom-element.mjs"), "chrome://global/content/ml/BlockWords.sys.mjs": typeof import("chrome://global/content/ml/BlockWords.sys.mjs"), @@ -44,12 +42,6 @@ export interface Modules { "chrome://global/content/ml/backends/OpenAIPipeline.mjs": typeof import("chrome://global/content/ml/backends/OpenAIPipeline.mjs"), "chrome://global/content/ml/backends/Pipeline.mjs": typeof import("chrome://global/content/ml/backends/Pipeline.mjs"), "chrome://global/content/ml/backends/StaticEmbeddingsPipeline.mjs": typeof import("chrome://global/content/ml/backends/StaticEmbeddingsPipeline.mjs"), - "chrome://global/content/ml/security/ConditionEvaluator.sys.mjs": typeof import("chrome://global/content/ml/security/ConditionEvaluator.sys.mjs"), - "chrome://global/content/ml/security/DecisionTypes.sys.mjs": typeof import("chrome://global/content/ml/security/DecisionTypes.sys.mjs"), - "chrome://global/content/ml/security/PolicyEvaluator.sys.mjs": typeof import("chrome://global/content/ml/security/PolicyEvaluator.sys.mjs"), - "chrome://global/content/ml/security/SecurityLogger.sys.mjs": typeof import("chrome://global/content/ml/security/SecurityLogger.sys.mjs"), - "chrome://global/content/ml/security/SecurityOrchestrator.sys.mjs": typeof import("chrome://global/content/ml/security/SecurityOrchestrator.sys.mjs"), - "chrome://global/content/ml/security/SecurityUtils.sys.mjs": typeof import("chrome://global/content/ml/security/SecurityUtils.sys.mjs"), "chrome://global/content/preferences/Preferences.mjs": typeof import("chrome://global/content/preferences/Preferences.mjs"), "chrome://global/content/translations/TranslationsTelemetry.sys.mjs": typeof import("chrome://global/content/translations/TranslationsTelemetry.sys.mjs"), "chrome://global/content/translations/TranslationsUtils.mjs": typeof import("chrome://global/content/translations/TranslationsUtils.mjs"), @@ -132,7 +124,6 @@ export interface Modules { "chrome://remote/content/shared/listeners/BeforeStopRequestListener.sys.mjs": typeof import("chrome://remote/content/shared/listeners/BeforeStopRequestListener.sys.mjs"), "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs": typeof import("chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs"), "chrome://remote/content/shared/listeners/CachedResourceListener.sys.mjs": typeof import("chrome://remote/content/shared/listeners/CachedResourceListener.sys.mjs"), - "chrome://remote/content/shared/listeners/ChromeWindowListener.sys.mjs": typeof import("chrome://remote/content/shared/listeners/ChromeWindowListener.sys.mjs"), "chrome://remote/content/shared/listeners/ConsoleAPIListener.sys.mjs": typeof import("chrome://remote/content/shared/listeners/ConsoleAPIListener.sys.mjs"), "chrome://remote/content/shared/listeners/ConsoleListener.sys.mjs": typeof import("chrome://remote/content/shared/listeners/ConsoleListener.sys.mjs"), "chrome://remote/content/shared/listeners/ContextualIdentityListener.sys.mjs": typeof import("chrome://remote/content/shared/listeners/ContextualIdentityListener.sys.mjs"), @@ -209,26 +200,20 @@ export interface Modules { "moz-src:///browser/components/StartupTelemetry.sys.mjs": typeof import("moz-src:///browser/components/StartupTelemetry.sys.mjs"), "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/Chat.sys.mjs"), "moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs"), - "moz-src:///browser/components/aiwindow/models/ConversationSuggestions.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/ConversationSuggestions.sys.mjs"), "moz-src:///browser/components/aiwindow/models/Insights.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/Insights.sys.mjs"), "moz-src:///browser/components/aiwindow/models/InsightsChatSource.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/InsightsChatSource.sys.mjs"), "moz-src:///browser/components/aiwindow/models/InsightsConstants.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/InsightsConstants.sys.mjs"), - "moz-src:///browser/components/aiwindow/models/InsightsConversationScheduler.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/InsightsConversationScheduler.sys.mjs"), "moz-src:///browser/components/aiwindow/models/InsightsDriftDetector.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/InsightsDriftDetector.sys.mjs"), - "moz-src:///browser/components/aiwindow/models/InsightsHistoryScheduler.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/InsightsHistoryScheduler.sys.mjs"), "moz-src:///browser/components/aiwindow/models/InsightsHistorySource.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/InsightsHistorySource.sys.mjs"), "moz-src:///browser/components/aiwindow/models/InsightsManager.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/InsightsManager.sys.mjs"), "moz-src:///browser/components/aiwindow/models/IntentClassifier.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/IntentClassifier.sys.mjs"), "moz-src:///browser/components/aiwindow/models/SearchBrowsingHistory.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/SearchBrowsingHistory.sys.mjs"), - "moz-src:///browser/components/aiwindow/models/SearchBrowsingHistoryDomainBoost.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/SearchBrowsingHistoryDomainBoost.sys.mjs"), "moz-src:///browser/components/aiwindow/models/TitleGeneration.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/TitleGeneration.sys.mjs"), "moz-src:///browser/components/aiwindow/models/Tools.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/Tools.sys.mjs"), "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/Utils.sys.mjs"), "moz-src:///browser/components/aiwindow/models/prompts/InsightsPrompts.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/models/prompts/InsightsPrompts.sys.mjs"), "moz-src:///browser/components/aiwindow/services/InsightStore.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/services/InsightStore.sys.mjs"), "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs"), - "moz-src:///browser/components/aiwindow/ui/modules/AIWindowAccountAuth.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/ui/modules/AIWindowAccountAuth.sys.mjs"), - "moz-src:///browser/components/aiwindow/ui/modules/AIWindowMenu.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/ui/modules/AIWindowMenu.sys.mjs"), "moz-src:///browser/components/aiwindow/ui/modules/ChatMessage.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/ui/modules/ChatMessage.sys.mjs"), "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs"), "moz-src:///browser/components/aiwindow/ui/modules/ChatUtils.sys.mjs": typeof import("moz-src:///browser/components/aiwindow/ui/modules/ChatUtils.sys.mjs"), @@ -367,8 +352,6 @@ export interface Modules { "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustWebextstorage.sys.mjs": typeof import("moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustWebextstorage.sys.mjs"), "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/tests/generated/RustUniffiBindingsTests.sys.mjs": typeof import("moz-src:///toolkit/components/uniffi-bindgen-gecko-js/tests/generated/RustUniffiBindingsTests.sys.mjs"), "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/tests/generated/RustUniffiBindingsTestsExternalTypes.sys.mjs": typeof import("moz-src:///toolkit/components/uniffi-bindgen-gecko-js/tests/generated/RustUniffiBindingsTestsExternalTypes.sys.mjs"), - "moz-src:///toolkit/modules/ColorPickerPanel.sys.mjs": typeof import("moz-src:///toolkit/modules/ColorPickerPanel.sys.mjs"), - "moz-src:///toolkit/modules/DateTimePickerPanel.sys.mjs": typeof import("moz-src:///toolkit/modules/DateTimePickerPanel.sys.mjs"), "moz-src:///toolkit/modules/PrefUtils.sys.mjs": typeof import("moz-src:///toolkit/modules/PrefUtils.sys.mjs"), "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs": typeof import("moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs"), "resource:///actors/AboutLoginsParent.sys.mjs": typeof import("resource:///actors/AboutLoginsParent.sys.mjs"), @@ -378,6 +361,7 @@ export interface Modules { "resource:///actors/AboutReaderParent.sys.mjs": typeof import("resource:///actors/AboutReaderParent.sys.mjs"), "resource:///actors/AboutWelcomeParent.sys.mjs": typeof import("resource:///actors/AboutWelcomeParent.sys.mjs"), "resource:///actors/ClickHandlerParent.sys.mjs": typeof import("resource:///actors/ClickHandlerParent.sys.mjs"), + "resource:///actors/ContentSearchParent.sys.mjs": typeof import("resource:///actors/ContentSearchParent.sys.mjs"), "resource:///actors/ContextMenuChild.sys.mjs": typeof import("resource:///actors/ContextMenuChild.sys.mjs"), "resource:///actors/LinkHandlerParent.sys.mjs": typeof import("resource:///actors/LinkHandlerParent.sys.mjs"), "resource:///actors/LinkPreviewChild.sys.mjs": typeof import("resource:///actors/LinkPreviewChild.sys.mjs"), @@ -493,7 +477,6 @@ export interface Modules { "resource:///modules/backup/BackupError.mjs": typeof import("resource:///modules/backup/BackupError.mjs"), "resource:///modules/backup/BackupResource.sys.mjs": typeof import("resource:///modules/backup/BackupResource.sys.mjs"), "resource:///modules/backup/BackupService.sys.mjs": typeof import("resource:///modules/backup/BackupService.sys.mjs"), - "resource:///modules/backup/BookmarksBackupResource.sys.mjs": typeof import("resource:///modules/backup/BookmarksBackupResource.sys.mjs"), "resource:///modules/backup/CookiesBackupResource.sys.mjs": typeof import("resource:///modules/backup/CookiesBackupResource.sys.mjs"), "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs": typeof import("resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs"), "resource:///modules/backup/FormHistoryBackupResource.sys.mjs": typeof import("resource:///modules/backup/FormHistoryBackupResource.sys.mjs"), @@ -502,7 +485,6 @@ export interface Modules { "resource:///modules/backup/PlacesBackupResource.sys.mjs": typeof import("resource:///modules/backup/PlacesBackupResource.sys.mjs"), "resource:///modules/backup/PreferencesBackupResource.sys.mjs": typeof import("resource:///modules/backup/PreferencesBackupResource.sys.mjs"), "resource:///modules/backup/SessionStoreBackupResource.sys.mjs": typeof import("resource:///modules/backup/SessionStoreBackupResource.sys.mjs"), - "resource:///modules/backup/SiteSettingsBackupResource.sys.mjs": typeof import("resource:///modules/backup/SiteSettingsBackupResource.sys.mjs"), "resource:///modules/distribution.sys.mjs": typeof import("resource:///modules/distribution.sys.mjs"), "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs": typeof import("resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs"), "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs": typeof import("resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"), @@ -719,6 +701,7 @@ export interface Modules { "resource://gre/modules/DAPSender.sys.mjs": typeof import("resource://gre/modules/DAPSender.sys.mjs"), "resource://gre/modules/DAPTelemetrySender.sys.mjs": typeof import("resource://gre/modules/DAPTelemetrySender.sys.mjs"), "resource://gre/modules/DAPVisitCounter.sys.mjs": typeof import("resource://gre/modules/DAPVisitCounter.sys.mjs"), + "resource://gre/modules/DateTimePickerPanel.sys.mjs": typeof import("resource://gre/modules/DateTimePickerPanel.sys.mjs"), "resource://gre/modules/DeferredTask.sys.mjs": typeof import("resource://gre/modules/DeferredTask.sys.mjs"), "resource://gre/modules/DelayedInit.sys.mjs": typeof import("resource://gre/modules/DelayedInit.sys.mjs"), "resource://gre/modules/DownloadCore.sys.mjs": typeof import("resource://gre/modules/DownloadCore.sys.mjs"), @@ -996,7 +979,6 @@ export interface Modules { "resource://gre/modules/narrate/NarrateControls.sys.mjs": typeof import("resource://gre/modules/narrate/NarrateControls.sys.mjs"), "resource://gre/modules/policies/WindowsGPOParser.sys.mjs": typeof import("resource://gre/modules/policies/WindowsGPOParser.sys.mjs"), "resource://gre/modules/policies/macOSPoliciesParser.sys.mjs": typeof import("resource://gre/modules/policies/macOSPoliciesParser.sys.mjs"), - "resource://gre/modules/psm/QWACs.sys.mjs": typeof import("resource://gre/modules/psm/QWACs.sys.mjs"), "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs": typeof import("resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs"), "resource://gre/modules/psm/X509.sys.mjs": typeof import("resource://gre/modules/psm/X509.sys.mjs"), "resource://gre/modules/psm/pippki.sys.mjs": typeof import("resource://gre/modules/psm/pippki.sys.mjs"), @@ -1234,7 +1216,6 @@ export interface Modules { "resource://test/non_shared_nest_import_shared_target_1.sys.mjs": typeof import("resource://test/non_shared_nest_import_shared_target_1.sys.mjs"), "resource://test/non_shared_nest_import_shared_target_2.sys.mjs": typeof import("resource://test/non_shared_nest_import_shared_target_2.sys.mjs"), "resource://test/not_found.mjs": typeof import("resource://test/not_found.mjs"), - "resource://testing-common/AIWindowTestUtils.sys.mjs": typeof import("resource://testing-common/AIWindowTestUtils.sys.mjs"), "resource://testing-common/AddonTestUtils.sys.mjs": typeof import("resource://testing-common/AddonTestUtils.sys.mjs"), "resource://testing-common/AllJavascriptTypes.mjs": typeof import("resource://testing-common/AllJavascriptTypes.mjs"), "resource://testing-common/AppData.sys.mjs": typeof import("resource://testing-common/AppData.sys.mjs"), diff --git a/tools/@types/generated/tspaths.json b/tools/@types/generated/tspaths.json @@ -17,12 +17,6 @@ "chrome://browser/content/aboutlogins/components/login-message-popup.mjs": [ "browser/components/aboutlogins/content/components/login-message-popup.mjs" ], - "chrome://browser/content/aiwindow/components/ai-chat-content.mjs": [ - "browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs" - ], - "chrome://browser/content/aiwindow/components/ai-chat-message.mjs": [ - "browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs" - ], "chrome://browser/content/aiwindow/components/input-cta.mjs": [ "browser/components/aiwindow/ui/components/input-cta/input-cta.mjs" ], @@ -101,18 +95,15 @@ "chrome://browser/content/ipprotection/ipprotection-status-card.mjs": [ "browser/components/ipprotection/content/ipprotection-status-card.mjs" ], + "chrome://browser/content/ipprotection/ipprotection-timer.mjs": [ + "browser/components/ipprotection/content/ipprotection-timer.mjs" + ], "chrome://browser/content/migration/migration-wizard-constants.mjs": [ "browser/components/migration/content/migration-wizard-constants.mjs" ], "chrome://browser/content/migration/migration-wizard.mjs": [ "browser/components/migration/content/migration-wizard.mjs" ], - "chrome://browser/content/multilineeditor/multiline-editor.mjs": [ - "browser/components/multilineeditor/multiline-editor.mjs" - ], - "chrome://browser/content/multilineeditor/prosemirror.bundle.mjs": [ - "third_party/js/prosemirror/prosemirror.bundle.mjs" - ], "chrome://browser/content/nsContextMenu.sys.mjs": [ "browser/base/content/nsContextMenu.sys.mjs" ], @@ -182,15 +173,6 @@ "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs": [ "browser/components/translations/content/TranslationsPanelShared.sys.mjs" ], - "chrome://browser/content/urlbar/SmartbarInput.mjs": [ - "browser/components/urlbar/content/SmartbarInput.mjs" - ], - "chrome://browser/content/urlbar/SmartbarInputController.mjs": [ - "browser/components/urlbar/content/SmartbarInputController.mjs" - ], - "chrome://browser/content/urlbar/SmartbarInputUtils.mjs": [ - "browser/components/urlbar/content/SmartbarInputUtils.mjs" - ], "chrome://browser/content/urlbar/UrlbarInput.mjs": [ "browser/components/urlbar/content/UrlbarInput.mjs" ], @@ -242,9 +224,6 @@ "chrome://global/content/aboutwebrtc/graphdb.mjs": [ "toolkit/content/aboutwebrtc/graphdb.mjs" ], - "chrome://global/content/bindings/colorpicker-common.mjs": [ - "toolkit/content/widgets/colorpicker-common.mjs" - ], "chrome://global/content/certviewer/certDecoder.mjs": [ "toolkit/components/certviewer/content/certDecoder.mjs" ], @@ -371,24 +350,6 @@ "chrome://global/content/ml/ort.webgpu-dev.mjs": [ "toolkit/components/ml/vendor/ort.webgpu-dev.mjs" ], - "chrome://global/content/ml/security/ConditionEvaluator.sys.mjs": [ - "toolkit/components/ml/security/ConditionEvaluator.sys.mjs" - ], - "chrome://global/content/ml/security/DecisionTypes.sys.mjs": [ - "toolkit/components/ml/security/DecisionTypes.sys.mjs" - ], - "chrome://global/content/ml/security/PolicyEvaluator.sys.mjs": [ - "toolkit/components/ml/security/PolicyEvaluator.sys.mjs" - ], - "chrome://global/content/ml/security/SecurityLogger.sys.mjs": [ - "toolkit/components/ml/security/SecurityLogger.sys.mjs" - ], - "chrome://global/content/ml/security/SecurityOrchestrator.sys.mjs": [ - "toolkit/components/ml/security/SecurityOrchestrator.sys.mjs" - ], - "chrome://global/content/ml/security/SecurityUtils.sys.mjs": [ - "toolkit/components/ml/security/SecurityUtils.sys.mjs" - ], "chrome://global/content/ml/transformers-dev.js": [ "toolkit/components/ml/vendor/transformers-dev.js" ], @@ -695,9 +656,6 @@ "chrome://remote/content/shared/listeners/CachedResourceListener.sys.mjs": [ "remote/shared/listeners/CachedResourceListener.sys.mjs" ], - "chrome://remote/content/shared/listeners/ChromeWindowListener.sys.mjs": [ - "remote/shared/listeners/ChromeWindowListener.sys.mjs" - ], "chrome://remote/content/shared/listeners/ConsoleAPIListener.sys.mjs": [ "remote/shared/listeners/ConsoleAPIListener.sys.mjs" ], @@ -935,6 +893,9 @@ "resource:///actors/ClickHandlerParent.sys.mjs": [ "browser/actors/ClickHandlerParent.sys.mjs" ], + "resource:///actors/ContentSearchParent.sys.mjs": [ + "browser/actors/ContentSearchParent.sys.mjs" + ], "resource:///actors/ContextMenuChild.sys.mjs": [ "browser/actors/ContextMenuChild.sys.mjs" ], @@ -1295,9 +1256,6 @@ "resource:///modules/backup/BackupService.sys.mjs": [ "browser/components/backup/BackupService.sys.mjs" ], - "resource:///modules/backup/BookmarksBackupResource.sys.mjs": [ - "browser/components/backup/resources/BookmarksBackupResource.sys.mjs" - ], "resource:///modules/backup/CookiesBackupResource.sys.mjs": [ "browser/components/backup/resources/CookiesBackupResource.sys.mjs" ], @@ -1322,9 +1280,6 @@ "resource:///modules/backup/SessionStoreBackupResource.sys.mjs": [ "browser/components/backup/resources/SessionStoreBackupResource.sys.mjs" ], - "resource:///modules/backup/SiteSettingsBackupResource.sys.mjs": [ - "browser/components/backup/resources/SiteSettingsBackupResource.sys.mjs" - ], "resource:///modules/distribution.sys.mjs": [ "browser/components/distribution.sys.mjs" ], @@ -5489,6 +5444,9 @@ "resource://gre/modules/DAPVisitCounter.sys.mjs": [ "toolkit/components/dap/DAPVisitCounter.sys.mjs" ], + "resource://gre/modules/DateTimePickerPanel.sys.mjs": [ + "toolkit/modules/DateTimePickerPanel.sys.mjs" + ], "resource://gre/modules/DeferredTask.sys.mjs": [ "toolkit/modules/DeferredTask.sys.mjs" ], @@ -6380,9 +6338,6 @@ "resource://gre/modules/psm/DER.sys.mjs": [ "security/manager/ssl/DER.sys.mjs" ], - "resource://gre/modules/psm/QWACs.sys.mjs": [ - "security/manager/ssl/QWACs.sys.mjs" - ], "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs": [ "security/manager/ssl/RemoteSecuritySettings.sys.mjs" ], @@ -7160,9 +7115,6 @@ "resource://test/webextension-helpers.js": [ "devtools/server/tests/xpcshell/webextension-helpers.js" ], - "resource://testing-common/AIWindowTestUtils.sys.mjs": [ - "browser/components/aiwindow/ui/test/AIWindowTestUtils.sys.mjs" - ], "resource://testing-common/AddonTestUtils.sys.mjs": [ "toolkit/mozapps/extensions/internal/AddonTestUtils.sys.mjs" ],