diff --git a/toolkit/components/console/hudservice/AutocompletePopup.jsm b/toolkit/components/console/hudservice/AutocompletePopup.jsm new file mode 100644 index 00000000000..7267c82c0fc --- /dev/null +++ b/toolkit/components/console/hudservice/AutocompletePopup.jsm @@ -0,0 +1,395 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Autocomplete Popup. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Mihai Sucan (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cu = Components.utils; + +// The XUL and XHTML namespace. +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +const HUD_STRINGS_URI = "chrome://global/locale/headsUpDisplay.properties"; + + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "stringBundle", function () { + return Services.strings.createBundle(HUD_STRINGS_URI); +}); + + +var EXPORTED_SYMBOLS = ["AutocompletePopup"]; + +/** + * Autocomplete popup UI implementation. + * + * @constructor + * @param nsIDOMDocument aDocument + * The document you want the popup attached to. + */ +function AutocompletePopup(aDocument) +{ + this._document = aDocument; + + // Reuse the existing popup elements. + this._panel = this._document.getElementById("webConsole_autocompletePopup"); + if (!this._panel) { + this._panel = this._document.createElementNS(XUL_NS, "panel"); + this._panel.setAttribute("id", "webConsole_autocompletePopup"); + this._panel.setAttribute("label", + stringBundle.GetStringFromName("Autocomplete.label")); + this._panel.setAttribute("noautofocus", "true"); + this._panel.setAttribute("ignorekeys", "true"); + + let mainPopupSet = this._document.getElementById("mainPopupSet"); + if (mainPopupSet) { + mainPopupSet.appendChild(this._panel); + } + else { + this._document.documentElement.appendChild(this._panel); + } + + this._list = this._document.createElementNS(XUL_NS, "richlistbox"); + this._list.flex = 1; + this._panel.appendChild(this._list); + + // Open and hide the panel, so we initialize the API of the richlistbox. + this._panel.width = 1; + this._panel.height = 1; + this._panel.openPopup(null, "overlap", 0, 0, false, false); + this._panel.hidePopup(); + this._panel.width = ""; + this._panel.height = ""; + } + else { + this._list = this._panel.firstChild; + } +} + +AutocompletePopup.prototype = { + _document: null, + _panel: null, + _list: null, + + /** + * Open the autocomplete popup panel. + * + * @param nsIDOMNode aAnchor + * Optional node to anchor the panel to. + */ + openPopup: function AP_openPopup(aAnchor) + { + this._panel.openPopup(aAnchor, "after_start", 0, 0, false, false); + + if (this.onSelect) { + this._list.addEventListener("select", this.onSelect, false); + } + + if (this.onClick) { + this._list.addEventListener("click", this.onClick, false); + } + + this._updateSize(); + }, + + /** + * Hide the autocomplete popup panel. + */ + hidePopup: function AP_hidePopup() + { + this._panel.hidePopup(); + + if (this.onSelect) { + this._list.removeEventListener("select", this.onSelect, false); + } + + if (this.onClick) { + this._list.removeEventListener("click", this.onClick, false); + } + }, + + /** + * Check if the autocomplete popup is open. + */ + get isOpen() { + return this._panel.state == "open"; + }, + + /** + * Destroy the object instance. Please note that the panel DOM elements remain + * in the DOM, because they might still be in use by other instances of the + * same code. It is the responsability of the client code to perform DOM + * cleanup. + */ + destroy: function AP_destroy() + { + if (this.isOpen) { + this.hidePopup(); + } + this.clearItems(); + + this._document = null; + this._list = null; + this._panel = null; + }, + + /** + * Get the autocomplete items array. + * + * @return array + * The array of autocomplete items. + */ + getItems: function AP_getItems() + { + let items = []; + + Array.forEach(this._list.childNodes, function(aItem) { + items.push(aItem._autocompleteItem); + }); + + return items; + }, + + /** + * Set the autocomplete items list, in one go. + * + * @param array aItems + * The list of items you want displayed in the popup list. + */ + setItems: function AP_setItems(aItems) + { + this.clearItems(); + aItems.forEach(this.appendItem, this); + + // Make sure that the new content is properly fitted by the XUL richlistbox. + if (this.isOpen) { + // We need the timeout to allow the content to reflow. Attempting to + // update the richlistbox size too early does not work. + this._document.defaultView.setTimeout(this._updateSize.bind(this), 1); + } + }, + + /** + * Update the panel size to fit the content. + * + * @private + */ + _updateSize: function AP__updateSize() + { + this._list.width = this._panel.clientWidth + + this._scrollbarWidth; + }, + + /** + * Clear all the items from the autocomplete list. + */ + clearItems: function AP_clearItems() + { + while (this._list.hasChildNodes()) { + this._list.removeChild(this._list.firstChild); + } + this._list.width = ""; + }, + + /** + * Getter for the index of the selected item. + * + * @type number + */ + get selectedIndex() { + return this._list.selectedIndex; + }, + + /** + * Setter for the selected index. + * + * @param number aIndex + * The number (index) of the item you want to select in the list. + */ + set selectedIndex(aIndex) { + this._list.selectedIndex = aIndex; + this._list.ensureIndexIsVisible(this._list.selectedIndex); + }, + + /** + * Getter for the selected item. + * @type object + */ + get selectedItem() { + return this._list.selectedItem ? + this._list.selectedItem._autocompleteItem : null; + }, + + /** + * Setter for the selected item. + * + * @param object aItem + * The object you want selected in the list. + */ + set selectedItem(aItem) { + this._list.selectedItem = this._findListItem(aItem); + this._list.ensureIndexIsVisible(this._list.selectedIndex); + }, + + /** + * Append an item into the autocomplete list. + * + * @param object aItem + * The item you want appended to the list. The object must have a + * "label" property which is used as the displayed value. + */ + appendItem: function AP_appendItem(aItem) + { + let description = this._document.createElementNS(XUL_NS, "description"); + description.textContent = aItem.label; + + let listItem = this._document.createElementNS(XUL_NS, "richlistitem"); + listItem.appendChild(description); + listItem._autocompleteItem = aItem; + + this._list.appendChild(listItem); + }, + + /** + * Find the richlistitem element that belongs to an item. + * + * @private + * + * @param object aItem + * The object you want found in the list. + * + * @return nsIDOMNode|null + * The nsIDOMNode that belongs to the given item object. This node is + * the richlistitem element. + */ + _findListItem: function AP__findListItem(aItem) + { + for (let i = 0; i < this._list.childNodes.length; i++) { + let child = this._list.childNodes[i]; + if (child._autocompleteItem == aItem) { + return child; + } + } + return null; + }, + + /** + * Remove an item from the popup list. + * + * @param object aItem + * The item you want removed. + */ + removeItem: function AP_removeItem(aItem) + { + let item = this._findListItem(aItem); + if (!item) { + throw new Error("Item not found!"); + } + this._list.removeChild(item); + }, + + /** + * Getter for the number of items in the popup. + * @type number + */ + get itemCount() { + return this._list.childNodes.length; + }, + + /** + * Select the next item in the list. + * + * @return object + * The newly selected item object. + */ + selectNextItem: function AP_selectNextItem() + { + if (this.selectedIndex < (this.itemCount - 1)) { + this.selectedIndex++; + } + else { + this.selectedIndex = -1; + } + + return this.selectedItem; + }, + + /** + * Select the previous item in the list. + * + * @return object + * The newly selected item object. + */ + selectPreviousItem: function AP_selectPreviousItem() + { + if (this.selectedIndex > -1) { + this.selectedIndex--; + } + else { + this.selectedIndex = this.itemCount - 1; + } + + return this.selectedItem; + }, + + /** + * Determine the scrollbar width in the current document. + * + * @private + */ + get _scrollbarWidth() + { + if (this.__scrollbarWidth) { + return this.__scrollbarWidth; + } + + let hbox = this._document.createElementNS(XUL_NS, "hbox"); + hbox.setAttribute("style", "height: 0%; overflow: hidden"); + + let scrollbar = this._document.createElementNS(XUL_NS, "scrollbar"); + scrollbar.setAttribute("orient", "vertical"); + hbox.appendChild(scrollbar); + + this._document.documentElement.appendChild(hbox); + this.__scrollbarWidth = scrollbar.clientWidth; + this._document.documentElement.removeChild(hbox); + + return this.__scrollbarWidth; + }, +}; + diff --git a/toolkit/components/console/hudservice/HUDService.jsm b/toolkit/components/console/hudservice/HUDService.jsm index 5f304b64d97..e1cac1e0fd8 100644 --- a/toolkit/components/console/hudservice/HUDService.jsm +++ b/toolkit/components/console/hudservice/HUDService.jsm @@ -86,6 +86,17 @@ XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () { return obj.PropertyPanel; }); +XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function () { + var obj = {}; + try { + Cu.import("resource://gre/modules/AutocompletePopup.jsm", obj); + } + catch (err) { + Cu.reportError(err); + } + return obj.AutocompletePopup; +}); + XPCOMUtils.defineLazyGetter(this, "namesAndValuesOf", function () { var obj = {}; Cu.import("resource:///modules/PropertyPanel.jsm", obj); @@ -524,8 +535,10 @@ ResponseListener.prototype = function createElement(aDocument, aTag, aAttributes) { let node = aDocument.createElement(aTag); - for (var attr in aAttributes) { - node.setAttribute(attr, aAttributes[attr]); + if (aAttributes) { + for (let attr in aAttributes) { + node.setAttribute(attr, aAttributes[attr]); + } } return node; } @@ -1763,6 +1776,8 @@ HUD_SERVICE.prototype = ownerDoc = outputNode.ownerDocument; ownerDoc.getElementById(id).parentNode.removeChild(outputNode); + this.hudReferences[id].jsterm.autocompletePopup.destroy(); + this.hudReferences[id].consoleWindowUnregisterOnHide = false; // remove the HeadsUpDisplay object from memory @@ -1791,6 +1806,12 @@ HUD_SERVICE.prototype = Services.obs.notifyObservers(id, "web-console-destroyed", null); if (Object.keys(this.hudReferences).length == 0) { + let autocompletePopup = outputNode.ownerDocument. + getElementById("webConsole_autocompletePopup"); + if (autocompletePopup) { + autocompletePopup.parentNode.removeChild(autocompletePopup); + } + this.suspend(); } }, @@ -4158,24 +4179,23 @@ function JSPropertyProvider(aScope, aInputValue) let properties = completionPart.split('.'); let matchProp; if (properties.length > 1) { - matchProp = properties[properties.length - 1].trimLeft(); - properties.pop(); - for each (var prop in properties) { - prop = prop.trim(); + matchProp = properties.pop().trimLeft(); + for (let i = 0; i < properties.length; i++) { + let prop = properties[i].trim(); - // If obj is undefined or null, then there is no change to run - // completion on it. Exit here. - if (typeof obj === "undefined" || obj === null) { - return null; - } - - // Check if prop is a getter function on obj. Functions can change other - // stuff so we can't execute them to get the next object. Stop here. - if (obj.__lookupGetter__(prop)) { - return null; - } - obj = obj[prop]; + // If obj is undefined or null, then there is no change to run completion + // on it. Exit here. + if (typeof obj === "undefined" || obj === null) { + return null; } + + // Check if prop is a getter function on obj. Functions can change other + // stuff so we can't execute them to get the next object. Stop here. + if (obj.__lookupGetter__(prop)) { + return null; + } + obj = obj[prop]; + } } else { matchProp = properties[0].trimLeft(); @@ -4193,17 +4213,15 @@ function JSPropertyProvider(aScope, aInputValue) } let matches = []; - for (var prop in obj) { - matches.push(prop); + for (let prop in obj) { + if (prop.indexOf(matchProp) == 0) { + matches.push(prop); + } } - matches = matches.filter(function(item) { - return item.indexOf(matchProp) == 0; - }).sort(); - return { matchProp: matchProp, - matches: matches + matches: matches.sort(), }; } @@ -4465,6 +4483,9 @@ function JSTerm(aContext, aParentNode, aMixin, aConsole) this.historyIndex = 0; this.historyPlaceHolder = 0; // this.history.length; this.log = LogFactory("*** JSTerm:"); + this.autocompletePopup = new AutocompletePopup(aParentNode.ownerDocument); + this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this); + this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this); this.init(); } @@ -4603,6 +4624,7 @@ JSTerm.prototype = { this.historyIndex++; this.historyPlaceHolder = this.history.length; this.setInputValue(""); + this.clearCompletion(); }, /** @@ -4957,53 +4979,56 @@ JSTerm.prototype = { return; } + let inputUpdated = false; + switch(aEvent.keyCode) { + case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE: + if (this.autocompletePopup.isOpen) { + this.clearCompletion(); + aEvent.preventDefault(); + } + break; + case Ci.nsIDOMKeyEvent.DOM_VK_RETURN: - this.execute(); + if (this.autocompletePopup.isOpen) { + this.acceptProposedCompletion(); + } + else { + this.execute(); + } aEvent.preventDefault(); break; case Ci.nsIDOMKeyEvent.DOM_VK_UP: - // history previous - if (this.canCaretGoPrevious()) { - let updated = this.historyPeruse(HISTORY_BACK); - if (updated && aEvent.cancelable) { - aEvent.preventDefault(); - } + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_BACKWARD); + } + else if (this.canCaretGoPrevious()) { + inputUpdated = this.historyPeruse(HISTORY_BACK); + } + if (inputUpdated) { + aEvent.preventDefault(); } break; case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: - // history next - if (this.canCaretGoNext()) { - let updated = this.historyPeruse(HISTORY_FORWARD); - if (updated && aEvent.cancelable) { - aEvent.preventDefault(); - } + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_FORWARD); + } + else if (this.canCaretGoNext()) { + inputUpdated = this.historyPeruse(HISTORY_FORWARD); + } + if (inputUpdated) { + aEvent.preventDefault(); } - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: - // accept proposed completion - this.acceptProposedCompletion(); break; case Ci.nsIDOMKeyEvent.DOM_VK_TAB: - // If there are more than one possible completion, pressing tab - // means taking the next completion, shift_tab means taking - // the previous completion. - var completionResult; - if (aEvent.shiftKey) { - completionResult = this.complete(this.COMPLETE_BACKWARD); - } - else { - completionResult = this.complete(this.COMPLETE_FORWARD); - } - if (completionResult) { - if (aEvent.cancelable) { - aEvent.preventDefault(); - } - aEvent.target.focus(); + // Generate a completion and accept the first proposed value. + if (this.complete(this.COMPLETE_HINT_ONLY) && + this.lastCompletion && + this.acceptProposedCompletion()) { + aEvent.preventDefault(); } break; @@ -5147,96 +5172,127 @@ JSTerm.prototype = { let inputValue = inputNode.value; // If the inputNode has no value, then don't try to complete on it. if (!inputValue) { - this.lastCompletion = null; - this.updateCompleteNode(""); + this.clearCompletion(); return false; } // Only complete if the selection is empty and at the end of the input. if (inputNode.selectionStart == inputNode.selectionEnd && inputNode.selectionEnd != inputValue.length) { - // TODO: shouldnt we do this in the other 'bail' cases? - this.lastCompletion = null; - this.updateCompleteNode(""); + this.clearCompletion(); return false; } - let matches; - let matchIndexToUse; - let matchOffset; + let popup = this.autocompletePopup; - // If there is a saved completion from last time and the used value for - // completion stayed the same, then use the stored completion. - if (this.lastCompletion && inputValue == this.lastCompletion.value) { - matches = this.lastCompletion.matches; - matchOffset = this.lastCompletion.matchOffset; - if (type === this.COMPLETE_BACKWARD) { - this.lastCompletion.index --; - } - else if (type === this.COMPLETE_FORWARD) { - this.lastCompletion.index ++; - } - matchIndexToUse = this.lastCompletion.index; - } - else { - // Look up possible completion values. - let completion = this.propertyProvider(this.sandbox.window, inputValue); - if (!completion) { - this.updateCompleteNode(""); + if (!this.lastCompletion || this.lastCompletion.value != inputValue) { + let properties = this.propertyProvider(this.sandbox.window, inputValue); + if (!properties || !properties.matches.length) { + this.clearCompletion(); return false; } - matches = completion.matches; - matchIndexToUse = 0; - matchOffset = completion.matchProp.length; - // Store this match; - this.lastCompletion = { - index: 0, - value: inputValue, - matches: matches, - matchOffset: matchOffset - }; - } - if (type != this.COMPLETE_HINT_ONLY && matches.length == 1) { - this.acceptProposedCompletion(); - return true; - } - else if (matches.length != 0) { - // Ensure that the matchIndexToUse is always a valid array index. - if (matchIndexToUse < 0) { - matchIndexToUse = matches.length + (matchIndexToUse % matches.length); - if (matchIndexToUse == matches.length) { - matchIndexToUse = 0; + let items = properties.matches.map(function(aMatch) { + return {label: aMatch}; + }); + popup.setItems(items); + this.lastCompletion = {value: inputValue, + matchProp: properties.matchProp}; + + if (items.length > 1 && !popup.isOpen) { + popup.openPopup(this.inputNode); + } + else if (items.length < 2 && popup.isOpen) { + popup.hidePopup(); + } + + if (items.length > 0) { + popup.selectedIndex = 0; + if (items.length == 1) { + // onSelect is not fired when the popup is not open. + this.onAutocompleteSelect(); } } - else { - matchIndexToUse = matchIndexToUse % matches.length; - } + } - let completionStr = matches[matchIndexToUse].substring(matchOffset); - this.updateCompleteNode(completionStr); - return completionStr ? true : false; + let accepted = false; + + if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { + this.acceptProposedCompletion(); + accepted = true; + } + else if (type == this.COMPLETE_BACKWARD) { + this.autocompletePopup.selectPreviousItem(); + } + else if (type == this.COMPLETE_FORWARD) { + this.autocompletePopup.selectNextItem(); + } + + return accepted || popup.itemCount > 0; + }, + + onAutocompleteSelect: function JSTF_onAutocompleteSelect() + { + let currentItem = this.autocompletePopup.selectedItem; + if (currentItem && this.lastCompletion) { + let suffix = currentItem.label.substring(this.lastCompletion. + matchProp.length); + this.updateCompleteNode(suffix); } else { this.updateCompleteNode(""); } - - return false; }, + /** + * Clear the current completion information and close the autocomplete popup, + * if needed. + */ + clearCompletion: function JSTF_clearCompletion() + { + this.autocompletePopup.clearItems(); + this.lastCompletion = null; + this.updateCompleteNode(""); + if (this.autocompletePopup.isOpen) { + this.autocompletePopup.hidePopup(); + } + }, + + /** + * Accept the proposed input completion. + * + * @return boolean + * True if there was a selected completion item and the input value + * was updated, false otherwise. + */ acceptProposedCompletion: function JSTF_acceptProposedCompletion() { - this.setInputValue(this.inputNode.value + this.completionValue); - this.updateCompleteNode(""); + let updated = false; + + let currentItem = this.autocompletePopup.selectedItem; + if (currentItem && this.lastCompletion) { + let suffix = currentItem.label.substring(this.lastCompletion. + matchProp.length); + this.setInputValue(this.inputNode.value + suffix); + updated = true; + } + + this.clearCompletion(); + + return updated; }, - updateCompleteNode: function JSTF_updateCompleteNode(suffix) + /** + * Update the node that displays the currently selected autocomplete proposal. + * + * @param string aSuffix + * The proposed suffix for the inputNode value. + */ + updateCompleteNode: function JSTF_updateCompleteNode(aSuffix) { - this.completionValue = suffix; - // completion prefix = input, with non-control chars replaced by spaces - let prefix = this.inputNode.value.replace(/[\S]/g, " "); - this.completeNode.value = prefix + this.completionValue; + let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : ""; + this.completeNode.value = prefix + aSuffix; }, }; diff --git a/toolkit/components/console/hudservice/Makefile.in b/toolkit/components/console/hudservice/Makefile.in index 821c9e01da5..46229d128d8 100644 --- a/toolkit/components/console/hudservice/Makefile.in +++ b/toolkit/components/console/hudservice/Makefile.in @@ -46,6 +46,7 @@ include $(DEPTH)/config/autoconf.mk EXTRA_JS_MODULES = HUDService.jsm \ PropertyPanel.jsm \ NetworkHelper.jsm \ + AutocompletePopup.jsm \ $(NULL) ifdef ENABLE_TESTS diff --git a/toolkit/components/console/hudservice/tests/browser/Makefile.in b/toolkit/components/console/hudservice/tests/browser/Makefile.in index 820b0403ad8..b72b3799fd7 100644 --- a/toolkit/components/console/hudservice/tests/browser/Makefile.in +++ b/toolkit/components/console/hudservice/tests/browser/Makefile.in @@ -138,6 +138,8 @@ _BROWSER_TEST_FILES = \ browser_webconsole_bug_646025_console_file_location.js \ browser_webconsole_position_ui.js \ browser_webconsole_bug_642615_autocomplete.js \ + browser_webconsole_bug_585991_autocomplete_popup.js \ + browser_webconsole_bug_585991_autocomplete_keys.js \ head.js \ $(NULL) diff --git a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_585991_autocomplete_keys.js b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_585991_autocomplete_keys.js new file mode 100644 index 00000000000..acdedc3892c --- /dev/null +++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_585991_autocomplete_keys.js @@ -0,0 +1,204 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Web Console test suite. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "data:text/html,

bug 585991 - autocomplete popup keyboard usage test"; + +function test() { + addTab(TEST_URI); + browser.addEventListener("load", tabLoaded, true); +} + +function tabLoaded() { + browser.removeEventListener("load", tabLoaded, true); + openConsole(); + + content.wrappedJSObject.foobarBug585991 = { + "item0": "value0", + "item1": "value1", + "item2": "value2", + "item3": "value3", + }; + + let hudId = HUDService.getHudIdByWindow(content); + HUD = HUDService.hudReferences[hudId]; + let jsterm = HUD.jsterm; + let popup = jsterm.autocompletePopup; + let completeNode = jsterm.completeNode; + + ok(!popup.isOpen, "popup is not open"); + + popup._panel.addEventListener("popupshown", function() { + popup._panel.removeEventListener("popupshown", arguments.callee, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 4, "popup.itemCount is correct"); + + let sameItems = popup.getItems(); + is(sameItems.every(function(aItem, aIndex) { + return aItem.label == "item" + aIndex; + }), true, "getItems returns back the same items"); + + let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "item0", "item0 is selected"); + is(completeNode.value, prefix + "item0", "completeNode.value holds item0"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem.label, "item1", "item1 is selected"); + is(completeNode.value, prefix + "item1", "completeNode.value holds item1"); + + EventUtils.synthesizeKey("VK_UP", {}); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "item0", "item0 is selected"); + is(completeNode.value, prefix + "item0", "completeNode.value holds item0"); + + popup._panel.addEventListener("popuphidden", autocompletePopupHidden, false); + + EventUtils.synthesizeKey("VK_TAB", {}); + }, false); + + jsterm.setInputValue("window.foobarBug585991"); + EventUtils.synthesizeKey(".", {}); +} + +function autocompletePopupHidden() +{ + let jsterm = HUD.jsterm; + let popup = jsterm.autocompletePopup; + let completeNode = jsterm.completeNode; + let inputNode = jsterm.inputNode; + + popup._panel.removeEventListener("popuphidden", arguments.callee, false); + + ok(!popup.isOpen, "popup is not open"); + + is(inputNode.value, "window.foobarBug585991.item0", + "completion was successful after VK_TAB"); + + ok(!completeNode.value, "completeNode is empty"); + + popup._panel.addEventListener("popupshown", function() { + popup._panel.removeEventListener("popupshown", arguments.callee, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 4, "popup.itemCount is correct"); + + let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "item0", "item0 is selected"); + is(completeNode.value, prefix + "item0", "completeNode.value holds item0"); + + popup._panel.addEventListener("popuphidden", function() { + popup._panel.removeEventListener("popuphidden", arguments.callee, false); + + ok(!popup.isOpen, "popup is not open after VK_ESCAPE"); + + is(inputNode.value, "window.foobarBug585991.", + "completion was cancelled"); + + ok(!completeNode.value, "completeNode is empty"); + + executeSoon(testReturnKey); + }, false); + + executeSoon(function() { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); + }, false); + + executeSoon(function() { + jsterm.setInputValue("window.foobarBug585991"); + EventUtils.synthesizeKey(".", {}); + }); +} + +function testReturnKey() +{ + let jsterm = HUD.jsterm; + let popup = jsterm.autocompletePopup; + let completeNode = jsterm.completeNode; + let inputNode = jsterm.inputNode; + + popup._panel.addEventListener("popupshown", function() { + popup._panel.removeEventListener("popupshown", arguments.callee, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 4, "popup.itemCount is correct"); + + let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); + + is(popup.selectedIndex, 0, "index 0 is selected"); + is(popup.selectedItem.label, "item0", "item0 is selected"); + is(completeNode.value, prefix + "item0", "completeNode.value holds item0"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem.label, "item1", "item1 is selected"); + is(completeNode.value, prefix + "item1", "completeNode.value holds item1"); + + popup._panel.addEventListener("popuphidden", function() { + popup._panel.removeEventListener("popuphidden", arguments.callee, false); + + ok(!popup.isOpen, "popup is not open after VK_RETURN"); + + is(inputNode.value, "window.foobarBug585991.item1", + "completion was successful after VK_RETURN"); + + ok(!completeNode.value, "completeNode is empty"); + + executeSoon(finishTest); + }, false); + + EventUtils.synthesizeKey("VK_RETURN", {}); + }, false); + + executeSoon(function() { + jsterm.setInputValue("window.foobarBug58599"); + EventUtils.synthesizeKey("1", {}); + EventUtils.synthesizeKey(".", {}); + }); +} diff --git a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_585991_autocomplete_popup.js b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_585991_autocomplete_popup.js new file mode 100644 index 00000000000..d89af7ee991 --- /dev/null +++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_585991_autocomplete_popup.js @@ -0,0 +1,129 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Web Console test suite. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const TEST_URI = "data:text/html,

bug 585991 - autocomplete popup test"; + +function test() { + addTab(TEST_URI); + browser.addEventListener("load", tabLoaded, true); +} + +function tabLoaded() { + browser.removeEventListener("load", tabLoaded, true); + openConsole(); + + let items = [ + {label: "item0", value: "value0"}, + {label: "item1", value: "value1"}, + {label: "item2", value: "value2"}, + ]; + + let hudId = HUDService.getHudIdByWindow(content); + let HUD = HUDService.hudReferences[hudId]; + let popup = HUD.jsterm.autocompletePopup; + + ok(!popup.isOpen, "popup is not open"); + + popup._panel.addEventListener("popupshown", function() { + popup._panel.removeEventListener("popupshown", arguments.callee, false); + + ok(popup.isOpen, "popup is open"); + + is(popup.itemCount, 0, "no items"); + + popup.setItems(items); + + is(popup.itemCount, items.length, "items added"); + + let sameItems = popup.getItems(); + is(sameItems.every(function(aItem, aIndex) { + return aItem === items[aIndex]; + }), true, "getItems returns back the same items"); + + is(popup.selectedIndex, -1, "no index is selected"); + ok(!popup.selectedItem, "no item is selected"); + + popup.selectedIndex = 1; + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem, items[1], "item1 is selected"); + + popup.selectedItem = items[2]; + + is(popup.selectedIndex, 2, "index 2 is selected"); + is(popup.selectedItem, items[2], "item2 is selected"); + + is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works"); + + is(popup.selectedIndex, 1, "index 1 is selected"); + is(popup.selectedItem, items[1], "item1 is selected"); + + is(popup.selectNextItem(), items[2], "selectPreviousItem() works"); + + is(popup.selectedIndex, 2, "index 2 is selected"); + is(popup.selectedItem, items[2], "item2 is selected"); + + ok(!popup.selectNextItem(), "selectPreviousItem() works"); + + is(popup.selectedIndex, -1, "no index is selected"); + ok(!popup.selectedItem, "no item is selected"); + + items.push({label: "label3", value: "value3"}); + popup.appendItem(items[3]); + + is(popup.itemCount, items.length, "item3 appended"); + + popup.selectedIndex = 3; + is(popup.selectedItem, items[3], "item3 is selected"); + + popup.removeItem(items[2]); + + is(popup.selectedIndex, 2, "index2 is selected"); + is(popup.selectedItem, items[3], "item3 is still selected"); + is(popup.itemCount, items.length - 1, "item2 removed"); + + popup.clearItems(); + is(popup.itemCount, 0, "items cleared"); + + popup.hidePopup(); + finishTest(); + }, false); + + popup.openPopup(); +} + diff --git a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_642615_autocomplete.js b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_642615_autocomplete.js index f625c80b3e2..89371c20905 100644 --- a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_642615_autocomplete.js +++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_642615_autocomplete.js @@ -20,7 +20,7 @@ function tabLoad(aEvent) { jsterm.clearOutput(); - ok(!jsterm.completionValue, "no completionValue"); + ok(!jsterm.completeNode.value, "no completeNode.value"); jsterm.setInputValue("doc"); @@ -28,28 +28,28 @@ function tabLoad(aEvent) { jsterm.inputNode.addEventListener("keyup", function() { jsterm.inputNode.removeEventListener("keyup", arguments.callee, false); - let completionValue = jsterm.completionValue; - ok(completionValue, "we have a completionValue"); + let completionValue = jsterm.completeNode.value; + ok(completionValue, "we have a completeNode.value"); // wait for paste jsterm.inputNode.addEventListener("input", function() { jsterm.inputNode.removeEventListener("input", arguments.callee, false); - ok(!jsterm.completionValue, "no completionValue after clipboard paste"); + ok(!jsterm.completeNode.value, "no completeNode.value after clipboard paste"); // wait for undo jsterm.inputNode.addEventListener("input", function() { jsterm.inputNode.removeEventListener("input", arguments.callee, false); - is(jsterm.completionValue, completionValue, - "same completionValue after undo"); + is(jsterm.completeNode.value, completionValue, + "same completeNode.value after undo"); // wait for paste (via keyboard event) jsterm.inputNode.addEventListener("keyup", function() { jsterm.inputNode.removeEventListener("keyup", arguments.callee, false); - ok(!jsterm.completionValue, - "no completionValue after clipboard paste (via keyboard event)"); + ok(!jsterm.completeNode.value, + "no completeNode.value after clipboard paste (via keyboard event)"); executeSoon(finishTest); }, false); diff --git a/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties b/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties index 22adcba3b68..fd7e1557ab0 100644 --- a/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties +++ b/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties @@ -139,6 +139,10 @@ webConsolePositionWindow=Window # title. webConsoleOwnWindowTitle=Web Console +# LOCALIZATION NOTE (Autocomplete.label): +# The autocomplete popup panel label/title. +Autocomplete.label=Autocomplete popup + # LOCALIZATION NOTE (stacktrace.anonymousFunction): # This string is used to display JavaScript functions that have no given name - # they are said to be anonymous. See stacktrace.outputMessage.