/* 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/. */ "use strict"; var EXPORTED_SYMBOLS = ["UrlbarInput"]; const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", ReaderMode: "resource://gre/modules/ReaderMode.jsm", Services: "resource://gre/modules/Services.jsm", UrlbarController: "resource:///modules/UrlbarController.jsm", UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.jsm", UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm", UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm", UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.jsm", UrlbarView: "resource:///modules/UrlbarView.jsm", }); XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); /** * Represents the urlbar . * Also forwards important textbox properties and methods. */ class UrlbarInput { /** * @param {object} options * The initial options for UrlbarInput. * @param {object} options.textbox * The element. * @param {object} options.panel * The element. * @param {UrlbarController} [options.controller] * Optional fake controller to override the built-in UrlbarController. * Intended for use in unit tests only. */ constructor(options = {}) { this.textbox = options.textbox; this.textbox.clickSelectsAll = UrlbarPrefs.get("clickSelectsAll"); this.panel = options.panel; this.window = this.textbox.ownerGlobal; this.document = this.window.document; this.controller = options.controller || new UrlbarController({ browserWindow: this.window, }); this.controller.setInput(this); this.view = new UrlbarView(this); this.valueIsTyped = false; this.userInitiatedFocus = false; this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.window); this.lastQueryContextPromise = Promise.resolve(); this._actionOverrideKeyCount = 0; this._resultForCurrentValue = null; this._suppressStartQuery = false; this._untrimmedValue = ""; // Forward textbox methods and properties. const METHODS = ["addEventListener", "removeEventListener", "setAttribute", "hasAttribute", "removeAttribute", "getAttribute", "select"]; const READ_ONLY_PROPERTIES = ["inputField", "editor"]; const READ_WRITE_PROPERTIES = ["placeholder", "readOnly", "selectionStart", "selectionEnd"]; for (let method of METHODS) { this[method] = (...args) => { return this.textbox[method](...args); }; } for (let property of READ_ONLY_PROPERTIES) { Object.defineProperty(this, property, { enumerable: true, get() { return this.textbox[property]; }, }); } for (let property of READ_WRITE_PROPERTIES) { Object.defineProperty(this, property, { enumerable: true, get() { return this.textbox[property]; }, set(val) { return this.textbox[property] = val; }, }); } XPCOMUtils.defineLazyGetter(this, "valueFormatter", () => { return new UrlbarValueFormatter(this); }); // The event bufferer handles some events, queues them up, and calls back // our handleEvent at the right time. this.eventBufferer = new UrlbarEventBufferer(this); this.inputField.addEventListener("blur", this.eventBufferer); this.inputField.addEventListener("keydown", this.eventBufferer); const inputFieldEvents = [ "focus", "input", "keyup", "mouseover", "paste", "scrollend", "select", "overflow", "underflow", ]; for (let name of inputFieldEvents) { this.inputField.addEventListener(name, this); } this.addEventListener("mousedown", this); this.view.panel.addEventListener("popupshowing", this); this.view.panel.addEventListener("popuphidden", this); this.inputField.controllers.insertControllerAt(0, new CopyCutController(this)); this._initPasteAndGo(); } /** * Shortens the given value, usually by removing http:// and trailing slashes, * such that calling nsIURIFixup::createFixupURI with the result will produce * the same URI. * * @param {string} val * The string to be trimmed if it appears to be URI * @returns {string} * The trimmed string */ trimValue(val) { return UrlbarPrefs.get("trimURLs") ? this.window.trimURL(val) : val; } /** * Applies styling to the text in the urlbar input, depending on the text. */ formatValue() { this.valueFormatter.update(); } closePopup() { this.controller.cancelQuery(); this.view.close(); } focus() { this.inputField.focus(); } blur() { this.inputField.blur(); } /** * Converts an internal URI (e.g. a wyciwyg URI) 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 = ReaderMode.getOriginalUrlObjectForDisplay(uri.displaySpec); if (readerStrippedURI) { return readerStrippedURI; } try { return Services.uriFixup.createExposableURI(uri); } catch (ex) {} return uri; } /** * Passes DOM events for the textbox to the _on_ methods. * @param {Event} event * DOM event from the . */ handleEvent(event) { let methodName = "_on_" + event.type; if (methodName in this) { this[methodName](event); } else { throw new Error("Unrecognized UrlbarInput event: " + event.type); } } /** * Handles an event which would cause a url or text to be opened. * XXX the name is currently handleCommand which is compatible with * urlbarBindings. However, it is no longer called automatically by autocomplete, * See _on_keydown. * * @param {Event} event The event triggering the open. * @param {string} [openWhere] Where we expect the result to be opened. * @param {object} [openParams] * The parameters related to where the result will be opened. * @param {object} [triggeringPrincipal] * The principal that the action was triggered from. */ handleCommand(event, openWhere, openParams = {}, triggeringPrincipal = null) { let isMouseEvent = event instanceof this.window.MouseEvent; 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. let selectedOneOff; if (this.view.isOpen) { selectedOneOff = this.view.oneOffSearchButtons.selectedButton; if (selectedOneOff && isMouseEvent && event.target != selectedOneOff) { selectedOneOff = null; } // Do the command of the selected one-off if it's not an engine. if (selectedOneOff && !selectedOneOff.engine) { selectedOneOff.doCommand(); return; } } // Use the selected result if we have one; this is usually the case // when the view is open. let index = this.view.selectedIndex; if (!selectedOneOff && index != -1) { this.pickResult(event, index); return; } let url; if (selectedOneOff) { // If there's a selected one-off button then load a search using // the button's engine. [url, openParams.postData] = UrlbarUtils.getSearchQueryUrl( selectedOneOff.engine, this._lastSearchString); this._recordSearch(selectedOneOff.engine, event); } else { // Use the current value if we don't have a UrlbarResult e.g. because the // view is closed. url = this.value; openParams.postData = null; } if (!url) { return; } let where = openWhere || this._whereToOpen(event); openParams.allowInheritPrincipal = false; // TODO: Work out how we get the user selection behavior, probably via passing // it in, since we don't have the old autocomplete controller to work with. // BrowserUsageTelemetry.recordUrlbarSelectedResultMethod( // event, this.userSelectionBehavior); url = this._maybeCanonizeURL(event, url) || url.trim(); try { new URL(url); } catch (ex) { let browser = this.window.gBrowser.selectedBrowser; let lastLocationChange = browser.lastLocationChange; UrlbarUtils.getShortcutOrURIAndPostData(url).then(data => { if (where != "current" || browser.lastLocationChange == lastLocationChange) { openParams.postData = data.postData; openParams.allowInheritPrincipal = data.mayInheritPrincipal; this._loadURL(data.url, where, openParams); } }); return; } this._loadURL(url, where, openParams); } handleRevert() { this.window.gBrowser.userTypedValue = null; this.window.URLBarSetURI(null, true); if (this.value && this.focused) { this.select(); } } /** * Called by the view when a result is picked. * * @param {Event} event The event that picked the result. * @param {resultIndex} resultIndex The index of the result that was picked. */ pickResult(event, resultIndex) { let result = this.view.getResult(resultIndex); this.setValueFromResult(result); this.view.close(); // TODO Bug 1500476: Work out how we get the user selection behavior, probably via passing // it in, since we don't have the old autocomplete controller to work with. // BrowserUsageTelemetry.recordUrlbarSelectedResultMethod( // event, this.userSelectionBehavior); this.controller.recordSelectedResult(event, result, resultIndex); let where = this._whereToOpen(event); let {url, postData} = UrlbarUtils.getUrlFromResult(result); let openParams = { postData, allowInheritPrincipal: false, }; if (result.autofill) { // For autofilled results, the value that should be canonized is not the // autofilled value but the value that the user typed. let canonizedUrl = this._maybeCanonizeURL(event, this._lastSearchString); if (canonizedUrl) { this._loadURL(canonizedUrl, where, openParams); return; } } switch (result.type) { case UrlbarUtils.RESULT_TYPE.TAB_SWITCH: { if (this.hasAttribute("actionoverride")) { where = "current"; break; } this.handleRevert(); let prevTab = this.window.gBrowser.selectedTab; let loadOpts = { adoptIntoActiveWindow: UrlbarPrefs.get("switchTabs.adoptIntoActiveWindow"), }; if (this.window.switchToTabHavingURI(Services.io.newURI(url), false, loadOpts) && prevTab.isEmpty) { this.window.gBrowser.removeTab(prevTab); } return; } case UrlbarUtils.RESULT_TYPE.SEARCH: { let canonizedUrl = this._maybeCanonizeURL(event, result.payload.suggestion || result.payload.query); if (canonizedUrl) { url = canonizedUrl; break; } const actionDetails = { isSuggestion: !!result.payload.suggestion, alias: result.payload.keyword, }; const engine = Services.search.getEngineByName(result.payload.engine); this._recordSearch(engine, event, actionDetails); break; } case UrlbarUtils.RESULT_TYPE.OMNIBOX: { // Give the extension control of handling the command. ExtensionSearchHandler.handleInputEntered(result.payload.keyword, result.payload.content, where); return; } } if (!url) { throw new Error(`Invalid url for result ${JSON.stringify(result)}`); } this._loadURL(url, where, openParams); } /** * Called by the view when moving through results with the keyboard. * * @param {UrlbarResult} result The result that was selected. */ setValueFromResult(result) { if (result.autofill) { this._setValueFromResultAutofill(result); } else { this.value = this._valueFromResultPayload(result); } this._resultForCurrentValue = result; // Also update userTypedValue. See bug 287996. this.window.gBrowser.userTypedValue = this.value; // The value setter clobbers the actiontype attribute, so update this after that. switch (result.type) { case UrlbarUtils.RESULT_TYPE.TAB_SWITCH: this.setAttribute("actiontype", "switchtab"); break; case UrlbarUtils.RESULT_TYPE.OMNIBOX: this.setAttribute("actiontype", "extension"); break; } } /** * Starts a query based on the user input. * * @param {number} [options.lastKey] * The last key the user entered (as a key code). */ startQuery({ lastKey = null, } = {}) { if (this._suppressStartQuery) { return; } let searchString = this.textValue; // If the user has deleted text at the end of the input since the last // query, then we don't want to autofill because doing so would autofill the // very text the user just deleted. let enableAutofill = UrlbarPrefs.get("autoFill") && (!this._lastSearchString || !this._lastSearchString.startsWith(searchString)); this._lastSearchString = searchString; // 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(new UrlbarQueryContext({ enableAutofill, isPrivate: this.isPrivate, lastKey, maxResults: UrlbarPrefs.get("maxRichResults"), muxer: "UnifiedComplete", providers: ["UnifiedComplete"], searchString, })); } /** * Sets the input's value, starts a search, and opens the popup. * * @param {string} value * The input's value will be set to this value, and the search will * use it as its query. */ search(value) { this.window.focusAndSelectUrlBar(); // If the value is a restricted token, append a space. if (Object.values(UrlbarTokenizer.RESTRICT).includes(value)) { this.inputField.value = value + " "; } else { this.inputField.value = value; } // Avoid selecting the text if this method is called twice in a row. this.selectionStart = -1; let event = this.document.createEvent("UIEvents"); event.initUIEvent("input", true, false, this.window, 0); this.inputField.dispatchEvent(event); } /** * Focus without the focus styles. * This is used by Activity Stream and about:privatebrowsing for search hand-off. */ setHiddenFocus() { this.textbox.classList.add("hidden-focus"); this.focus(); } /** * Remove the hidden focus styles. * This is used by Activity Stream and about:privatebrowsing for search hand-off. */ removeHiddenFocus() { this.textbox.classList.remove("hidden-focus"); } // Getters and Setters below. get focused() { return this.textbox.getAttribute("focused") == "true"; } get goButton() { return this.document.getAnonymousElementByAttribute(this.textbox, "anonid", "urlbar-go-button"); } get textValue() { return this.inputField.value; } get value() { return this._untrimmedValue; } set value(val) { this._untrimmedValue = val; let originalUrl = ReaderMode.getOriginalUrlObjectForDisplay(val); if (originalUrl) { val = originalUrl.displaySpec; } val = this.trimValue(val); this.valueIsTyped = false; this._resultForCurrentValue = null; this.inputField.value = val; this.formatValue(); 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; } // Private methods below. _setValueFromResultAutofill(result) { this.value = result.autofill.value; this.selectionStart = result.autofill.selectionStart; this.selectionEnd = result.autofill.selectionEnd; } _valueFromResultPayload(result) { switch (result.type) { case UrlbarUtils.RESULT_TYPE.KEYWORD: return result.payload.input; case UrlbarUtils.RESULT_TYPE.SEARCH: return (result.payload.keyword ? result.payload.keyword + " " : "") + (result.payload.suggestion || result.payload.query); case UrlbarUtils.RESULT_TYPE.OMNIBOX: return result.payload.content; } try { let uri = Services.io.newURI(result.payload.url); if (uri) { return this.window.losslessDecodeURI(uri); } } catch (ex) {} return ""; } _updateTextOverflow() { if (!this._overflowing) { this.removeAttribute("textoverflow"); return; } this.window.promiseDocumentFlushed(() => { // Check overflow again to ensure it didn't change in the meantime. let input = this.inputField; if (input && this._overflowing) { let side = input.scrollLeft && input.scrollLeft == input.scrollLeftMax ? "start" : "end"; 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.value); } } _getSelectedValueForClipboard() { let selection = this.editor.selection; const flags = Ci.nsIDocumentEncoder.OutputPreformatted | Ci.nsIDocumentEncoder.OutputRaw; let selectedVal = selection.toStringWithFormat("text/plain", flags, 0); // Handle multiple-range selection as a string for simplicity. if (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. if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "") { 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.textValue.replace(selectedVal, ""); if (remainder != "" && remainder[0] != "/") { return selectedVal; } } let uri; if (this.getAttribute("pageproxystate") == "valid") { uri = this.window.gBrowser.currentURI; } else { // We're dealing with an autocompleted value. if (!this._resultForCurrentValue) { throw new Error("UrlbarInput: Should have a UrlbarResult since " + "pageproxystate != 'valid' and valueIsTyped == false"); } let resultURL = this._resultForCurrentValue.payload.url; if (!resultURL) { return selectedVal; } try { uri = Services.uriFixup.createFixupURI(resultURL, Services.uriFixup.FIXUP_FLAG_NONE); } catch (e) {} if (!uri) { return selectedVal; } } uri = this.makeURIReadable(uri); // 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.textValue == selectedVal && !uri.schemeIs("javascript") && !uri.schemeIs("data") && !UrlbarPrefs.get("decodeURLsOnCopy")) { return uri.displaySpec; } // Just the beginning of the URL is selected, or we want a decoded // url. First check for a trimmed value. let spec = uri.displaySpec; let trimmedSpec = this.trimValue(spec); if (spec != trimmedSpec) { // Prepend the portion that trimValue removed from the beginning. // This assumes trimValue will only truncate the URL at // the beginning or end (or both). let trimmedSegments = spec.split(trimmedSpec); selectedVal = trimmedSegments[0] + selectedVal; } 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.setAttribute("actionoverride", "true"); this.view.panel.setAttribute("actionoverride", "true"); } else if (this._actionOverrideKeyCount && --this._actionOverrideKeyCount == 0) { this.removeAttribute("actionoverride"); this.view.panel.removeAttribute("actionoverride"); } } } /** * Get the url to load for the search query and records in telemetry that it * is being loaded. * * @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 {alias} searchActionDetails.alias * True if this query was initiated via a search alias. */ _recordSearch(engine, event, searchActionDetails = {}) { const isOneOff = this.view.oneOffSearchButtons.maybeRecordTelemetry(event); // Infer the type of the event which triggered the search. let eventType = "unknown"; if (event instanceof KeyboardEvent) { eventType = "key"; } else if (event instanceof MouseEvent) { eventType = "mouse"; } // Augment the search action details object. let details = searchActionDetails; details.isOneOff = isOneOff; details.type = eventType; this.window.BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details); } /** * 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 (!(event instanceof KeyboardEvent) || !event.ctrlKey || !UrlbarPrefs.get("ctrlCanonizesURLs") || !/^\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; } value = "http://www." + value; this.value = value; return value; } /** * 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 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] * If the principal may be inherited */ _loadURL(url, openUILinkWhere, params) { let browser = this.window.gBrowser.selectedBrowser; // TODO: These should probably be set by the input field. // this.value = url; // browser.userTypedValue = url; if (this.window.gInitialPages.includes(url)) { browser.initialPageLoadedFromUserAction = url; } try { 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. Cu.reportError(ex); } 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; } // 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. browser.focus(); if (openUILinkWhere != "current") { this.handleRevert(); } 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(); } } // TODO This should probably be handed via input. // Ensure the start of the URL is visible for usability reasons. // this.selectionStart = this.selectionEnd = 0; this.closePopup(); } /** * 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 isMouseEvent = event instanceof MouseEvent; let reuseEmpty = !isMouseEvent; let where = undefined; if (!isMouseEvent && event && event.altKey) { // 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 (!isMouseEvent && event && event.ctrlKey && UrlbarPrefs.get("ctrlCanonizesURLs")) { // If we're allowing canonization, and this is a key event with ctrl // pressed, open in current tab to allow ctrl-enter to canonize URL. where = "current"; } else { where = this.window.whereToOpenLink(event, false, false); } if (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; } _initPasteAndGo() { let inputBox = this.document.getAnonymousElementByAttribute( this.textbox, "anonid", "moz-input-box"); // Force the Custom Element to upgrade until Bug 1470242 handles this: this.window.customElements.upgrade(inputBox); let contextMenu = inputBox.menupopup; let insertLocation = contextMenu.firstElementChild; while (insertLocation.nextElementSibling && insertLocation.getAttribute("cmd") != "cmd_paste") { insertLocation = insertLocation.nextElementSibling; } if (!insertLocation) { return; } let pasteAndGo = this.document.createXULElement("menuitem"); 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.handleCommand(); this._suppressStartQuery = false; }); contextMenu.addEventListener("popupshowing", () => { 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); } // Event handlers below. _on_blur(event) { this.formatValue(); this.closePopup(); } _on_focus(event) { this._updateUrlTooltip(); this.formatValue(); } _on_mouseover(event) { this._updateUrlTooltip(); } _on_mousedown(event) { if (event.originalTarget == this.inputField && event.button == 0 && event.detail == 2 && UrlbarPrefs.get("doubleClickSelectsAll")) { this.editor.selectAll(); event.preventDefault(); return; } if (event.originalTarget.classList.contains("urlbar-history-dropmarker") && event.button == 0) { if (this.view.isOpen) { this.view.close(); } else { this.startQuery(); } } } _on_input() { let value = this.textValue; this.valueIsTyped = true; this._untrimmedValue = value; this.window.gBrowser.userTypedValue = value; if (value) { this.setAttribute("usertyping", "true"); } else { this.removeAttribute("usertyping"); } this.removeAttribute("actiontype"); // XXX Fill in lastKey, and add anything else we need. this.startQuery({ lastKey: null, }); } _on_select(event) { if (!Services.clipboard.supportsSelectionClipboard()) { return; } if (!this.window.windowUtils.isHandlingUserInput) { return; } let val = this._getSelectedValueForClipboard(); if (!val) { return; } ClipboardHelper.copyStringToClipboard(val, Services.clipboard.kSelectionClipboard); } _on_overflow(event) { const targetIsPlaceholder = !event.originalTarget.classList.contains("anonymous-div"); // 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.classList.contains("anonymous-div"); // 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.inputField.value; let oldStart = oldValue.substring(0, this.inputField.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.inputField.selectionEnd); let pasteData = UrlbarUtils.stripUnsafeProtocolOnPaste(originalPasteData); if (originalPasteData != pasteData) { // Unfortunately we're not allowed to set the bits being pasted // so cancel this event: event.preventDefault(); event.stopImmediatePropagation(); this.inputField.value = oldStart + pasteData + oldEnd; // Fix up cursor/selection: let newCursorPos = oldStart.length + pasteData.length; this.inputField.selectionStart = newCursorPos; this.inputField.selectionEnd = newCursorPos; } } _on_scrollend(event) { this._updateTextOverflow(); } _on_TabSelect(event) { this.controller.tabContextChanged(); } _on_keydown(event) { this.controller.handleKeyNavigation(event); this._toggleActionOverride(event); } _on_keyup(event) { this._toggleActionOverride(event); } _on_popupshowing() { this.setAttribute("open", "true"); } _on_popuphidden() { this.removeAttribute("open"); } } /** * Handles copy and cut commands for the urlbar. */ class CopyCutController { /** * @param {UrlbarInput} urlbar * The UrlbarInput 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.selectionStart = urlbar.selectionEnd = start; let event = urlbar.document.createEvent("UIEvents"); event.initUIEvent("input", true, false, urlbar.window, 0); urlbar.inputField.dispatchEvent(event); } ClipboardHelper.copyString(val); } /** * @param {string} command * @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 * @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() {} }