summaryrefslogtreecommitdiffstats
path: root/xpfe/components/autocomplete
diff options
context:
space:
mode:
Diffstat (limited to 'xpfe/components/autocomplete')
-rw-r--r--xpfe/components/autocomplete/jar.mn9
-rw-r--r--xpfe/components/autocomplete/moz.build7
-rw-r--r--xpfe/components/autocomplete/resources/content/autocomplete.css46
-rw-r--r--xpfe/components/autocomplete/resources/content/autocomplete.xml1646
4 files changed, 1708 insertions, 0 deletions
diff --git a/xpfe/components/autocomplete/jar.mn b/xpfe/components/autocomplete/jar.mn
new file mode 100644
index 000000000..d3ddf8a61
--- /dev/null
+++ b/xpfe/components/autocomplete/jar.mn
@@ -0,0 +1,9 @@
+# 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/.
+
+toolkit.jar:
+ content/global/autocomplete.xml (resources/content/autocomplete.xml)
+
+comm.jar:
+ content/communicator/autocomplete.css (resources/content/autocomplete.css)
diff --git a/xpfe/components/autocomplete/moz.build b/xpfe/components/autocomplete/moz.build
new file mode 100644
index 000000000..eb4454d28
--- /dev/null
+++ b/xpfe/components/autocomplete/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/xpfe/components/autocomplete/resources/content/autocomplete.css b/xpfe/components/autocomplete/resources/content/autocomplete.css
new file mode 100644
index 000000000..6c67bad2e
--- /dev/null
+++ b/xpfe/components/autocomplete/resources/content/autocomplete.css
@@ -0,0 +1,46 @@
+/* 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/. */
+
+
+.autocomplete-result-popupset {
+ width: 0 !important;
+}
+
+.autocomplete-result-popup {
+ display: -moz-popup !important;
+}
+
+/* the C++ implementation of widgets is too eager to make popups visible.
+ this causes problems (bug 120155 and others), thus this workaround: */
+.autocomplete-result-popup[hidden="true"] {
+ visibility: hidden;
+}
+
+.autocomplete-tree {
+ -moz-user-focus: ignore;
+}
+
+.autocomplete-history-dropmarker {
+ display: none;
+}
+
+.autocomplete-history-dropmarker[enablehistory="true"] {
+ display: -moz-box;
+}
+
+/* The following rule is here to fix bug 96899 (and now 117952).
+ Somehow trees create a situation
+ in which a popupset flows itself as if its popup child is directly within it
+ instead of the placeholder child that should actually be inside the popupset.
+ This is a stopgap measure, and it does not address the real bug. */
+popupset {
+ max-width: 0px;
+ width: 0px;
+ min-width: 0%;
+ min-height: 0%;
+}
+
+treecolpicker {
+ display: none;
+}
diff --git a/xpfe/components/autocomplete/resources/content/autocomplete.xml b/xpfe/components/autocomplete/resources/content/autocomplete.xml
new file mode 100644
index 000000000..93b6dfdb0
--- /dev/null
+++ b/xpfe/components/autocomplete/resources/content/autocomplete.xml
@@ -0,0 +1,1646 @@
+<?xml version="1.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/. -->
+
+
+<bindings id="autocompleteBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="autocomplete" role="xul:combobox"
+ extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <resources>
+ <stylesheet src="chrome://communicator/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content>
+ <children includes="menupopup"/>
+
+ <xul:hbox class="autocomplete-textbox-container" flex="1" align="center">
+ <children includes="image|deck|stack|box">
+ <xul:image class="autocomplete-icon" allowevents="true"/>
+ </children>
+
+ <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,tooltiptext=inputtooltiptext">
+ <children/>
+ <html:input anonid="input" class="autocomplete-textbox textbox-input"
+ allowevents="true"
+ xbl:inherits="tooltiptext=inputtooltiptext,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint,userAction"/>
+ </xul:hbox>
+ <children includes="hbox"/>
+ </xul:hbox>
+
+ <xul:dropmarker class="autocomplete-history-dropmarker" allowevents="true"
+ xbl:inherits="open,enablehistory" anonid="historydropmarker"/>
+
+ <xul:popupset>
+ <xul:panel type="autocomplete" anonid="popup"
+ ignorekeys="true" noautofocus="true" level="top"
+ xbl:inherits="for=id,nomatch"/>
+ </xul:popupset>
+ </content>
+
+ <implementation implements="nsIDOMXULMenuListElement">
+
+ <constructor><![CDATA[
+ // XXX bug 90337 band-aid until we figure out what's going on here
+ if (this.value != this.mInputElt.value)
+ this.mInputElt.value = this.value;
+ delete this.value;
+
+ // listen for pastes
+ this.mInputElt.controllers.insertControllerAt(0, this.mPasteController);
+
+ // listen for menubar activation
+ window.top.addEventListener("DOMMenuBarActive", this.mMenuBarListener, true);
+
+ // set default property values
+ this.ifSetAttribute("timeout", 50);
+ this.ifSetAttribute("pastetimeout", 1000);
+ this.ifSetAttribute("maxrows", 5);
+ this.ifSetAttribute("showpopup", true);
+ this.ifSetAttribute("disableKeyNavigation", true);
+
+ // initialize the search sessions
+ if (this.hasAttribute("autocompletesearch"))
+ this.initAutoCompleteSearch();
+
+ // hack to work around lack of bottom-up constructor calling
+ if ("initialize" in this.popup)
+ this.popup.initialize();
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this.clearResults(false);
+ window.top.removeEventListener("DOMMenuBarActive", this.mMenuBarListener, true);
+ this.mInputElt.controllers.removeController(this.mPasteController);
+ ]]></destructor>
+
+ <!-- =================== nsIAutoCompleteInput =================== -->
+ <!-- XXX: This implementation is currently incomplete. -->
+
+ <!-- reference to the results popup element -->
+ <field name="popup"><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid", "popup");
+ ]]></field>
+
+ <property name="popupOpen"
+ onget="return this.mMenuOpen;"
+ onset="if (val) this.openPopup(); else this.closePopup(); return val;"/>
+
+ <!-- option to turn off autocomplete -->
+ <property name="disableAutoComplete"
+ onset="this.setAttribute('disableautocomplete', val); return val;"
+ onget="return this.getAttribute('disableautocomplete') == 'true';"/>
+
+ <!-- if the resulting match string is not at the beginning of the typed string,
+ this will optionally autofill like this "bar |>> foobar|" -->
+ <property name="completeDefaultIndex"
+ onset="this.setAttribute('completedefaultindex', val); return val;"
+ onget="return this.getAttribute('completedefaultindex') == 'true';"/>
+
+ <!-- option for completing to the default result whenever the user hits
+ enter or the textbox loses focus -->
+ <property name="forceComplete"
+ onset="this.setAttribute('forcecomplete', val); return val;"
+ onget="return this.getAttribute('forcecomplete') == 'true';"/>
+
+ <property name="minResultsForPopup"
+ onset="this.setAttribute('minresultsforpopup', val); return val;"
+ onget="var t = this.getAttribute('minresultsforpopup'); return t ? parseInt(t) : 1;"/>
+
+ <!-- maximum number of rows to display -->
+ <property name="maxRows"
+ onset="this.setAttribute('maxrows', val); return val;"
+ onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>
+
+ <!-- toggles a second column in the results list which contains
+ the string in the comment field of each autocomplete result -->
+ <property name="showCommentColumn"
+ onget="return this.getAttribute('showcommentcolumn') == 'true';">
+ <setter><![CDATA[
+ this.popup.showCommentColumn = val;
+ this.setAttribute('showcommentcolumn', val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- number of milliseconds after a keystroke before a search begins -->
+ <property name="timeout"
+ onset="this.setAttribute('timeout', val); return val;"
+ onget="return parseInt(this.getAttribute('timeout')) || 0;"/>
+
+ <property name="searchParam"
+ onget="return this.getAttribute('autocompletesearchparam') || '';"
+ onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
+
+ <property name="searchCount" readonly="true"
+ onget="return this.sessionCount;"/>
+
+ <method name="getSearchAt">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var idx = -1;
+ for (var name in this.mSessions)
+ if (++idx == aIndex)
+ return name;
+
+ return null;
+ ]]></body>
+ </method>
+
+ <property name="textValue"
+ onget="return this.value;"
+ onset="this.setTextValue(val); return val;"/>
+
+ <method name="onSearchBegin">
+ <body><![CDATA[
+ this._fireEvent("searchbegin");
+ ]]></body>
+ </method>
+
+ <method name="onSearchComplete">
+ <body><![CDATA[
+ if (this.noMatch)
+ this.setAttribute("nomatch", "true");
+ else
+ this.removeAttribute("nomatch");
+
+ this._fireEvent("searchcomplete");
+ ]]></body>
+ </method>
+
+ <method name="onTextReverted">
+ <body><![CDATA[
+ return this._fireEvent("textreverted");
+ ]]></body>
+ </method>
+
+ <!-- =================== nsIDOMXULMenuListElement =================== -->
+
+ <property name="editable" readonly="true"
+ onget="return true;" />
+
+ <property name="crop"
+ onset="this.setAttribute('crop', val); return val;"
+ onget="return this.getAttribute('crop');"/>
+
+ <property name="label" readonly="true"
+ onget="return this.mInputElt.value;"/>
+
+ <property name="open"
+ onget="return this.getAttribute('open') == 'true';">
+ <setter>
+ <![CDATA[
+ var historyPopup = document.getAnonymousElementByAttribute(this, "anonid", "historydropmarker");
+ if (val) {
+ this.setAttribute('open', true);
+ historyPopup.showPopup();
+ } else {
+ this.removeAttribute('open');
+ historyPopup.hidePopup();
+ }
+ ]]>
+ </setter>
+ </property>
+
+ <!-- =================== PUBLIC PROPERTIES =================== -->
+
+ <property name="value"
+ onget="return this.mInputElt.value;">
+ <setter><![CDATA[
+ this.ignoreInputEvent = true;
+ this.mInputElt.value = val;
+ this.ignoreInputEvent = false;
+ var event = document.createEvent('Events');
+ event.initEvent('ValueChange', true, true);
+ this.mInputElt.dispatchEvent(event);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="focused"
+ onget="return this.getAttribute('focused') == 'true';"/>
+
+ <method name="initAutoCompleteSearch">
+ <body><![CDATA[
+ var list = this.getAttribute("autocompletesearch").split(" ");
+ for (var i = 0; i < list.length; i++) {
+ var name = list[i];
+ var contractid = "@mozilla.org/autocomplete/search;1?name=" + name;
+ if (contractid in Components.classes) {
+ try {
+ this.mSessions[name] =
+ Components.classes[contractid].getService(Components.interfaces.nsIAutoCompleteSearch);
+ this.mLastResults[name] = null;
+ this.mLastRows[name] = 0;
+ ++this.sessionCount;
+ } catch (e) {
+ dump("### ERROR - unable to create search \"" + name + "\".\n");
+ }
+ } else {
+ dump("search \"" + name + "\" not found - skipping.\n");
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- the number of sessions currently in use -->
+ <field name="sessionCount">0</field>
+
+ <!-- number of milliseconds after a paste before a search begins -->
+ <property name="pasteTimeout"
+ onset="this.setAttribute('pastetimeout', val); return val;"
+ onget="var t = parseInt(this.getAttribute('pastetimeout')); return t ? t : 0;"/>
+
+ <!-- option for filling the textbox with the best match while typing
+ and selecting the difference -->
+ <property name="autoFill"
+ onset="this.setAttribute('autofill', val); return val;"
+ onget="return this.getAttribute('autofill') == 'true';"/>
+
+ <!-- if this attribute is set, allow different style for
+ non auto-completed lines -->
+ <property name="highlightNonMatches"
+ onset="this.setAttribute('highlightnonmatches', val); return val;"
+ onget="return this.getAttribute('highlightnonmatches') == 'true';"/>
+
+ <!-- option to show the popup containing the results -->
+ <property name="showPopup"
+ onset="this.setAttribute('showpopup', val); return val;"
+ onget="return this.getAttribute('showpopup') == 'true';"/>
+
+ <!-- option to allow scrolling through the list via the tab key, rather than
+ tab moving focus out of the textbox -->
+ <property name="tabScrolling"
+ onset="this.setAttribute('tabscrolling', val); return val;"
+ onget="return this.getAttribute('tabscrolling') == 'true';"/>
+
+ <!-- option to completely ignore any blur events while
+ searches are still going on. This is useful so that nothing
+ gets autopicked if the window is required to lose focus for
+ some reason (eg in LDAP autocomplete, another window may be
+ brought up so that the user can enter a password to authenticate
+ to an LDAP server). -->
+ <property name="ignoreBlurWhileSearching"
+ onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
+ onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>
+
+ <!-- state which indicates the current action being performed by the user.
+ Possible values are : none, typing, scrolling -->
+ <property name="userAction"
+ onset="this.setAttribute('userAction', val); return val;"
+ onget="return this.getAttribute('userAction');"/>
+
+ <!-- state which indicates if the last search had no matches -->
+ <field name="noMatch">true</field>
+
+ <!-- state which indicates a search is currently happening -->
+ <field name="isSearching">false</field>
+
+ <!-- state which indicates a search timeout is current waiting -->
+ <property name="isWaiting"
+ onget="return this.mAutoCompleteTimer != 0;"/>
+
+ <!-- =================== PRIVATE PROPERTIES =================== -->
+
+ <field name="mSessions">({})</field>
+ <field name="mLastResults">({})</field>
+ <field name="mLastRows">({})</field>
+ <field name="mLastKeyCode">null</field>
+ <field name="mAutoCompleteTimer">0</field>
+ <field name="mMenuOpen">false</field>
+ <field name="mFireAfterSearch">false</field>
+ <field name="mFinishAfterSearch">false</field>
+ <field name="mNeedToFinish">false</field>
+ <field name="mNeedToComplete">false</field>
+ <field name="mTransientValue">false</field>
+ <field name="mView">null</field>
+ <field name="currentSearchString">""</field>
+ <field name="ignoreInputEvent">false</field>
+ <field name="oninit">null</field>
+ <field name="mDefaultMatchFilled">false</field>
+ <field name="mFirstReturn">true</field>
+ <field name="mIsPasting">false</field>
+
+ <field name="mPasteController"><![CDATA[
+ ({
+ self: this,
+ kGlobalClipboard: Components.interfaces.nsIClipboard.kGlobalClipboard,
+ supportsCommand: function(aCommand) {
+ return aCommand == "cmd_paste";
+ },
+ isCommandEnabled: function(aCommand) {
+ return aCommand == "cmd_paste" &&
+ this.self.editor.isSelectionEditable &&
+ this.self.editor.canPaste(this.kGlobalClipboard);
+ },
+ doCommand: function(aCommand) {
+ if (aCommand == "cmd_paste") {
+ this.self.mIsPasting = true;
+ this.self.editor.paste(this.kGlobalClipboard);
+ this.self.mIsPasting = false;
+ }
+ },
+ onEvent: function() {}
+ })
+ ]]></field>
+
+ <field name="mMenuBarListener"><![CDATA[
+ ({
+ self: this,
+ handleEvent: function(aEvent) {
+ try {
+ this.self.finishAutoComplete(false, false, aEvent);
+ this.self.clearTimer();
+ this.self.closePopup();
+ } catch (e) {
+ window.top.removeEventListener("DOMMenuBarActive", this, true);
+ }
+ }
+ })
+ ]]></field>
+
+ <field name="mAutoCompleteObserver"><![CDATA[
+ ({
+ self: this,
+ onSearchResult: function(aSearch, aResult) {
+ for (var name in this.self.mSessions)
+ if (this.self.mSessions[name] == aSearch)
+ this.self.processResults(name, aResult);
+ }
+ })
+ ]]></field>
+
+ <field name="mInputElt"><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid", "input");
+ ]]></field>
+
+ <field name="mMenuAccessKey"><![CDATA[
+ Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch)
+ .getIntPref("ui.key.menuAccessKey");
+ ]]></field>
+
+ <!-- =================== PUBLIC METHODS =================== -->
+
+ <method name="getErrorAt">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var obj = aIndex < 0 ? null : this.convertIndexToSession(aIndex);
+ return obj && this.mLastResults[obj.session] &&
+ this.mLastResults[obj.session].errorDescription;
+ ]]></body>
+ </method>
+
+ <!-- get a value from the autocomplete results as a string via an absolute index-->
+ <method name="getResultValueAt">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var obj = this.convertIndexToSession(aIndex);
+ return obj ? this.getSessionValueAt(obj.session, obj.index) : null;
+ ]]></body>
+ </method>
+
+ <!-- get a value from the autocomplete results as a string from a specific session -->
+ <method name="getSessionValueAt">
+ <parameter name="aSession"/>
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ var result = this.mLastResults[aSession];
+ return result.errorDescription || result.getValueAt(aIndex);
+ ]]></body>
+ </method>
+
+ <!-- get the total number of results overall -->
+ <method name="getResultCount">
+ <body><![CDATA[
+ return this.view.rowCount;
+ ]]></body>
+ </method>
+
+ <!-- get the first session that has results -->
+ <method name="getDefaultSession">
+ <body><![CDATA[
+ for (var name in this.mLastResults) {
+ var results = this.mLastResults[name];
+ if (results && results.matchCount > 0 && !results.errorDescription)
+ return name;
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <!-- empty the cached result data and empty the results popup -->
+ <method name="clearResults">
+ <parameter name="aInvalidate"/>
+ <body><![CDATA[
+ this.clearResultData();
+ this.clearResultElements(aInvalidate);
+ ]]></body>
+ </method>
+
+ <!-- =================== PRIVATE METHODS =================== -->
+
+ <!-- ::::::::::::: session searching ::::::::::::: -->
+
+ <!-- -->
+ <method name="callListener">
+ <parameter name="me"/>
+ <parameter name="aAction"/>
+ <body><![CDATA[
+ // bail if the binding was detached or the element removed from
+ // document during the timeout
+ if (!("startLookup" in me) || !me.ownerDocument || !me.parentNode)
+ return;
+
+ me.clearTimer();
+
+ if (me.disableAutoComplete)
+ return;
+
+ switch (aAction) {
+ case "startLookup":
+ me.startLookup();
+ break;
+
+ case "stopLookup":
+ me.stopLookup();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="startLookup">
+ <body><![CDATA[
+ var str = this.currentSearchString;
+ if (!str) {
+ this.clearResults(false);
+ this.closePopup();
+ return;
+ }
+
+ this.isSearching = true;
+ this.mFirstReturn = true;
+ this.mSessionReturns = this.sessionCount;
+ this.mFailureItems = 0;
+ this.mDefaultMatchFilled = false; // clear out our prefill state.
+
+ // Notify the input that the search is beginning.
+ this.onSearchBegin();
+
+ // tell each session to start searching...
+ for (var name in this.mSessions)
+ try {
+ this.mSessions[name].startSearch(str, this.searchParam, this.mLastResults[name], this.mAutoCompleteObserver);
+ } catch (e) {
+ --this.mSessionReturns;
+ this.searchFailed();
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="stopLookup">
+ <body><![CDATA[
+ for (var name in this.mSessions)
+ this.mSessions[name].stopSearch();
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="processResults">
+ <parameter name="aSessionName"/>
+ <parameter name="aResults"/>
+ <body><![CDATA[
+ if (this.disableAutoComplete)
+ return;
+
+ const ACR = Components.interfaces.nsIAutoCompleteResult;
+ var status = aResults.searchResult;
+ if (status != ACR.RESULT_NOMATCH_ONGOING &&
+ status != ACR.RESULT_SUCCESS_ONGOING)
+ --this.mSessionReturns;
+
+ // check the many criteria for failure
+ if (aResults.errorDescription)
+ ++this.mFailureItems;
+ else if (status == ACR.RESULT_IGNORED ||
+ status == ACR.RESULT_FAILURE ||
+ status == ACR.RESULT_NOMATCH ||
+ status == ACR.RESULT_NOMATCH_ONGOING ||
+ aResults.matchCount == 0 ||
+ aResults.searchString != this.currentSearchString)
+ {
+ this.mLastResults[aSessionName] = null;
+ if (this.mFirstReturn)
+ this.clearResultElements(false);
+ this.mFirstReturn = false;
+ this.searchFailed();
+ return;
+ }
+
+ if (this.mFirstReturn) {
+ if (this.view.mTree)
+ this.view.mTree.beginUpdateBatch();
+ this.clearResultElements(false); // clear results, but don't repaint yet
+ }
+
+ // always call openPopup...we may not have opened it
+ // if a previous search session didn't return enough search results.
+ // it's smart and doesn't try to open itself multiple times...
+ // be sure to add our result elements before calling openPopup as we need
+ // to know the total # of results found so far.
+ this.addResultElements(aSessionName, aResults);
+
+ this.autoFillInput(aSessionName, aResults, false);
+ if (this.mFirstReturn && this.view.mTree)
+ this.view.mTree.endUpdateBatch();
+ this.openPopup();
+ this.mFirstReturn = false;
+
+ // if this is the last session to return...
+ if (this.mSessionReturns == 0)
+ this.postSearchCleanup();
+
+ if (this.mFinishAfterSearch)
+ this.finishAutoComplete(false, this.mFireAfterSearch, null);
+ ]]></body>
+ </method>
+
+ <!-- called each time a search fails, except when failure items need
+ to be displayed. If all searches have failed, clear the list
+ and close the popup -->
+ <method name="searchFailed">
+ <body><![CDATA[
+ // if all searches are done and they all failed...
+ if (this.mSessionReturns == 0 && this.getResultCount() == 0) {
+ if (this.minResultsForPopup == 0) {
+ this.clearResults(true); // clear data and repaint empty
+ this.openPopup();
+ } else {
+ this.closePopup();
+ }
+ }
+
+ // if it's the last session to return, time to clean up...
+ if (this.mSessionReturns == 0)
+ this.postSearchCleanup();
+ ]]></body>
+ </method>
+
+ <!-- does some stuff after a search is done (success or failure) -->
+ <method name="postSearchCleanup">
+ <body><![CDATA[
+ this.isSearching = false;
+
+ // figure out if there are no matches in all search sessions
+ var failed = true;
+ for (var name in this.mSessions) {
+ if (this.mLastResults[name])
+ failed = this.mLastResults[name].errorDescription ||
+ this.mLastResults[name].matchCount == 0;
+ if (!failed)
+ break;
+ }
+ this.noMatch = failed;
+
+ // if we have processed all of our searches, and none of them gave us a default index,
+ // then we should try to auto fill the input field with the first match.
+ // note: autoFillInput is smart enough to kick out if we've already prefilled something...
+ if (!this.noMatch) {
+ var defaultSession = this.getDefaultSession();
+ if (defaultSession)
+ this.autoFillInput(defaultSession, this.mLastResults[defaultSession], true);
+ }
+
+ // Notify the input that the search is complete.
+ this.onSearchComplete();
+ ]]></body>
+ </method>
+
+ <!-- when the focus exits the widget or user hits return,
+ determine what value to leave in the textbox -->
+ <method name="finishAutoComplete">
+ <parameter name="aForceComplete"/>
+ <parameter name="aFireTextCommand"/>
+ <parameter name="aTriggeringEvent"/>
+ <body><![CDATA[
+ this.mFinishAfterSearch = false;
+ this.mFireAfterSearch = false;
+ if (this.mNeedToFinish && !this.disableAutoComplete) {
+ // set textbox value to either override value, or default search result
+ var val = this.popup.overrideValue;
+ if (val) {
+ this.setTextValue(val);
+ this.mNeedToFinish = false;
+ } else if (this.mTransientValue ||
+ !(this.forceComplete ||
+ (aForceComplete &&
+ this.mDefaultMatchFilled &&
+ this.mNeedToComplete))) {
+ this.mNeedToFinish = false;
+ } else if (this.isWaiting) {
+ // if the user typed, the search results are out of date, so let
+ // the search finish, and tell it to come back here when it's done
+ this.mFinishAfterSearch = true;
+ this.mFireAfterSearch = aFireTextCommand;
+ return;
+ } else {
+ // we want to use the default item index for the first session which gave us a valid
+ // default item index...
+ for (var name in this.mLastResults) {
+ var results = this.mLastResults[name];
+ if (results && results.matchCount > 0 &&
+ !results.errorDescription && results.defaultIndex != -1)
+ {
+ val = results.getValueAt(results.defaultIndex);
+ this.setTextValue(val);
+ this.mDefaultMatchFilled = true;
+ this.mNeedToFinish = false;
+ break;
+ }
+ }
+
+ if (this.mNeedToFinish) {
+ // if a search is happening at this juncture, bail out of this function
+ // and let the search finish, and tell it to come back here when it's done
+ if (this.isSearching) {
+ this.mFinishAfterSearch = true;
+ this.mFireAfterSearch = aFireTextCommand;
+ return;
+ }
+
+ this.mNeedToFinish = false;
+ var defaultSession = this.getDefaultSession();
+ if (defaultSession)
+ {
+ // preselect the first one
+ var first = this.getSessionValueAt(defaultSession, 0);
+ this.setTextValue(first);
+ this.mDefaultMatchFilled = true;
+ }
+ }
+ }
+
+ this.stopLookup();
+
+ this.closePopup();
+ }
+
+ this.mNeedToComplete = false;
+ this.clearTimer();
+
+ if (aFireTextCommand)
+ this._fireEvent("textentered", this.userAction, aTriggeringEvent);
+ ]]></body>
+ </method>
+
+ <!-- when the user clicks an entry in the autocomplete popup -->
+ <method name="onResultClick">
+ <body><![CDATA[
+ // set textbox value to either override value, or the clicked result
+ var errItem = this.getErrorAt(this.popup.selectedIndex);
+ var val = this.popup.overrideValue;
+ if (val)
+ this.setTextValue(val);
+ else if (this.popup.selectedIndex != -1) {
+ if (errItem) {
+ this.setTextValue(this.currentSearchString);
+ this.mTransientValue = true;
+ } else {
+ this.setTextValue(this.getResultValueAt(
+ this.popup.selectedIndex));
+ }
+ }
+
+ this.mNeedToFinish = false;
+ this.mNeedToComplete = false;
+
+ this.closePopup();
+
+ this.currentSearchString = "";
+
+ if (errItem)
+ this._fireEvent("errorcommand", errItem);
+ this._fireEvent("textentered", "clicking");
+ ]]></body>
+ </method>
+
+ <!-- when the user hits escape, revert the previously typed value in the textbox -->
+ <method name="undoAutoComplete">
+ <body><![CDATA[
+ var val = this.currentSearchString;
+
+ var ok = this.onTextReverted();
+ if ((ok || ok == undefined) && val)
+ this.setTextValue(val);
+
+ this.userAction = "typing";
+
+ this.currentSearchString = this.value;
+ this.mNeedToComplete = false;
+ ]]></body>
+ </method>
+
+ <!-- convert an absolute result index into a session name/index pair -->
+ <method name="convertIndexToSession">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ for (var name in this.mLastRows) {
+ if (aIndex < this.mLastRows[name])
+ return { session: name, index: aIndex };
+ aIndex -= this.mLastRows[name];
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: user input handling ::::::::::::: -->
+
+ <!-- -->
+ <method name="processInput">
+ <body><![CDATA[
+ // stop current lookup in case it's async.
+ this.stopLookup();
+ // stop the queued up lookup on a timer
+ this.clearTimer();
+
+ if (this.disableAutoComplete)
+ return;
+
+ this.userAction = "typing";
+ this.mFinishAfterSearch = false;
+ this.mNeedToFinish = true;
+ this.mTransientValue = false;
+ this.mNeedToComplete = true;
+ var str = this.value;
+ this.currentSearchString = str;
+ this.popup.clearSelection();
+
+ var timeout = this.mIsPasting ? this.pasteTimeout : this.timeout;
+ this.mAutoCompleteTimer = setTimeout(this.callListener, timeout, this, "startLookup");
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="processKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ this.mLastKeyCode = aEvent.keyCode;
+
+ var killEvent = false;
+
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_TAB:
+ if (this.tabScrolling) {
+ // don't kill this event if alt-tab or ctrl-tab is hit
+ if (!aEvent.altKey && !aEvent.ctrlKey) {
+ killEvent = this.mMenuOpen;
+ if (killEvent)
+ this.keyNavigation(aEvent);
+ }
+ }
+ break;
+
+ case KeyEvent.DOM_VK_RETURN:
+
+ // if this is a failure item, save it for fireErrorCommand
+ var errItem = this.getErrorAt(this.popup.selectedIndex);
+
+ killEvent = this.mMenuOpen;
+ this.finishAutoComplete(true, true, aEvent);
+ this.closePopup();
+ if (errItem) {
+ this._fireEvent("errorcommand", errItem);
+ }
+ break;
+
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.clearTimer();
+ killEvent = this.mMenuOpen;
+ this.undoAutoComplete();
+ this.closePopup();
+ break;
+
+ case KeyEvent.DOM_VK_LEFT:
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_HOME:
+ case KeyEvent.DOM_VK_END:
+ this.finishAutoComplete(true, false, aEvent);
+ this.clearTimer();
+ this.closePopup();
+ break;
+
+ case KeyEvent.DOM_VK_DOWN:
+ if (!aEvent.altKey) {
+ this.clearTimer();
+ killEvent = this.keyNavigation(aEvent);
+ break;
+ }
+ // Alt+Down falls through to history popup toggling code
+
+ case KeyEvent.DOM_VK_F4:
+ if (!aEvent.ctrlKey && !aEvent.shiftKey && this.getAttribute("enablehistory") == "true") {
+ var historyPopup = document.getAnonymousElementByAttribute(this, "anonid", "historydropmarker");
+ if (historyPopup)
+ historyPopup.showPopup();
+ else
+ historyPopup.hidePopup();
+ }
+ break;
+ case KeyEvent.DOM_VK_PAGE_UP:
+ case KeyEvent.DOM_VK_PAGE_DOWN:
+ case KeyEvent.DOM_VK_UP:
+ if (!aEvent.ctrlKey && !aEvent.metaKey) {
+ this.clearTimer();
+ killEvent = this.keyNavigation(aEvent);
+ }
+ break;
+
+ case KeyEvent.DOM_VK_BACK_SPACE:
+ if (!aEvent.ctrlKey && !aEvent.altKey && !aEvent.shiftKey &&
+ this.selectionStart == this.currentSearchString.length &&
+ this.selectionEnd == this.value.length &&
+ this.mDefaultMatchFilled) {
+ this.mDefaultMatchFilled = false;
+ this.value = this.currentSearchString;
+ }
+
+ if (!/Mac/.test(navigator.platform))
+ break;
+ case KeyEvent.DOM_VK_DELETE:
+ if (/Mac/.test(navigator.platform) && !aEvent.shiftKey)
+ break;
+
+ if (this.mMenuOpen && this.popup.selectedIndex != -1) {
+ var obj = this.convertIndexToSession(this.popup.selectedIndex);
+ if (obj) {
+ var result = this.mLastResults[obj.session];
+ if (!result.errorDescription) {
+ var count = result.matchCount;
+ result.removeValueAt(obj.index, true);
+ this.view.updateResults(this.popup.selectedIndex, result.matchCount - count);
+ killEvent = true;
+ }
+ }
+ }
+ break;
+ }
+
+ if (killEvent) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+
+ return true;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="processStartComposition">
+ <body><![CDATA[
+ this.finishAutoComplete(false, false, null);
+ this.clearTimer();
+ this.closePopup();
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="keyNavigation">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var k = aEvent.keyCode;
+ if (k == KeyEvent.DOM_VK_TAB ||
+ k == KeyEvent.DOM_VK_UP || k == KeyEvent.DOM_VK_DOWN ||
+ k == KeyEvent.DOM_VK_PAGE_UP || k == KeyEvent.DOM_VK_PAGE_DOWN)
+ {
+ if (!this.mMenuOpen) {
+ // Original xpfe style was to allow the up and down keys to have
+ // their default Mac action if the popup could not be opened.
+ // For compatibility for toolkit we now have to predict which
+ // keys have a default action that we can always allow to fire.
+ if (/Mac/.test(navigator.platform) &&
+ ((k == KeyEvent.DOM_VK_UP &&
+ (this.selectionStart != 0 ||
+ this.selectionEnd != 0)) ||
+ (k == KeyEvent.DOM_VK_DOWN &&
+ (this.selectionStart != this.value.length ||
+ this.selectionEnd != this.value.length))))
+ return false;
+ if (this.currentSearchString != this.value) {
+ this.processInput();
+ return true;
+ }
+ if (this.view.rowCount < this.minResultsForPopup)
+ return true; // used to be false, see above
+
+ this.mNeedToFinish = true;
+ this.openPopup();
+ return true;
+ }
+
+ this.userAction = "scrolling";
+ this.mNeedToComplete = false;
+
+ var reverse = k == KeyEvent.DOM_VK_TAB && aEvent.shiftKey ||
+ k == KeyEvent.DOM_VK_UP ||
+ k == KeyEvent.DOM_VK_PAGE_UP;
+ var page = k == KeyEvent.DOM_VK_PAGE_UP ||
+ k == KeyEvent.DOM_VK_PAGE_DOWN;
+ var selected = this.popup.selectBy(reverse, page);
+
+ // determine which value to place in the textbox
+ this.ignoreInputEvent = true;
+ if (selected != -1) {
+ if (this.getErrorAt(selected)) {
+ if (this.currentSearchString)
+ this.setTextValue(this.currentSearchString);
+ } else {
+ this.setTextValue(this.getResultValueAt(selected));
+ }
+ this.mTransientValue = true;
+ } else {
+ if (this.currentSearchString)
+ this.setTextValue(this.currentSearchString);
+ this.mTransientValue = false;
+ }
+
+ // move cursor to the end
+ this.mInputElt.setSelectionRange(this.value.length, this.value.length);
+ this.ignoreInputEvent = false;
+ }
+ return true;
+ ]]></body>
+ </method>
+
+ <!-- while the user is typing, fill the textbox with the "default" value
+ if one can be assumed, and select the end of the text -->
+ <method name="autoFillInput">
+ <parameter name="aSessionName"/>
+ <parameter name="aResults"/>
+ <parameter name="aUseFirstMatchIfNoDefault"/>
+ <body><![CDATA[
+ if (this.mInputElt.selectionEnd < this.currentSearchString.length ||
+ this.mDefaultMatchFilled)
+ return;
+
+ if (!this.mFinishAfterSearch &&
+ (this.autoFill || this.completeDefaultIndex) &&
+ this.mLastKeyCode != KeyEvent.DOM_VK_BACK_SPACE &&
+ this.mLastKeyCode != KeyEvent.DOM_VK_DELETE) {
+ var indexToUse = aResults.defaultIndex;
+ if (aUseFirstMatchIfNoDefault && indexToUse == -1)
+ indexToUse = 0;
+
+ if (indexToUse != -1) {
+ var resultValue = this.getSessionValueAt(aSessionName, indexToUse);
+ var match = resultValue.toLowerCase();
+ var entry = this.currentSearchString.toLowerCase();
+ this.ignoreInputEvent = true;
+ if (match.indexOf(entry) == 0) {
+ var endPoint = this.value.length;
+ this.setTextValue(this.value + resultValue.substr(endPoint));
+ this.mInputElt.setSelectionRange(endPoint, this.value.length);
+ } else {
+ if (this.completeDefaultIndex) {
+ this.setTextValue(this.value + " >> " + resultValue);
+ this.mInputElt.setSelectionRange(entry.length, this.value.length);
+ } else {
+ var postIndex = resultValue.indexOf(this.value);
+ if (postIndex >= 0) {
+ var startPt = this.value.length;
+ this.setTextValue(this.value +
+ resultValue.substr(startPt+postIndex));
+ this.mInputElt.setSelectionRange(startPt, this.value.length);
+ }
+ }
+ }
+ this.mNeedToComplete = true;
+ this.ignoreInputEvent = false;
+ this.mDefaultMatchFilled = true;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: popup and tree ::::::::::::: -->
+
+ <!-- -->
+ <method name="openPopup">
+ <body><![CDATA[
+ if (!this.mMenuOpen && this.focused &&
+ (this.getResultCount() >= this.minResultsForPopup ||
+ this.mFailureItems)) {
+ var w = this.boxObject.width;
+ if (w != this.popup.boxObject.width)
+ this.popup.setAttribute("width", w);
+ this.popup.showPopup(this, -1, -1, "popup", "bottomleft", "topleft");
+ this.mMenuOpen = true;
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="closePopup">
+ <body><![CDATA[
+ if (this.popup && this.mMenuOpen) {
+ this.popup.hidePopup();
+ this.mMenuOpen = false;
+ }
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="addResultElements">
+ <parameter name="aSession"/>
+ <parameter name="aResults"/>
+ <body><![CDATA[
+ var count = aResults.errorDescription ? 1 : aResults.matchCount;
+ if (this.focused && this.showPopup) {
+ var row = 0;
+ for (var name in this.mSessions) {
+ row += this.mLastRows[name];
+ if (name == aSession)
+ break;
+ }
+ this.view.updateResults(row, count - this.mLastRows[name]);
+ this.popup.adjustHeight();
+ }
+ this.mLastResults[aSession] = aResults;
+ this.mLastRows[aSession] = count;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="clearResultElements">
+ <parameter name="aInvalidate"/>
+ <body><![CDATA[
+ for (var name in this.mSessions)
+ this.mLastRows[name] = 0;
+ this.view.clearResults();
+ if (aInvalidate)
+ this.popup.adjustHeight();
+
+ this.noMatch = true;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="setTextValue">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this.value = aValue;
+
+ // Completing a result should simulate the user typing the result,
+ // so fire an input event.
+ var evt = document.createEvent("UIEvents");
+ evt.initUIEvent("input", true, false, window, 0);
+ var oldIgnoreInput = this.ignoreInputEvent;
+ this.ignoreInputEvent = true;
+ this.dispatchEvent(evt);
+ this.ignoreInputEvent = oldIgnoreInput;
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="clearResultData">
+ <body><![CDATA[
+ for (var name in this.mSessions)
+ this.mLastResults[name] = null;
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: miscellaneous ::::::::::::: -->
+
+ <!-- -->
+ <method name="ifSetAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aVal"/>
+ <body><![CDATA[
+ if (!this.hasAttribute(aAttr))
+ this.setAttribute(aAttr, aVal);
+ ]]></body>
+ </method>
+
+ <!-- -->
+ <method name="clearTimer">
+ <body><![CDATA[
+ if (this.mAutoCompleteTimer) {
+ clearTimeout(this.mAutoCompleteTimer);
+ this.mAutoCompleteTimer = 0;
+ }
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: event dispatching ::::::::::::: -->
+
+ <method name="_fireEvent">
+ <parameter name="aEventType"/>
+ <parameter name="aEventParam"/>
+ <parameter name="aTriggeringEvent"/>
+ <body>
+ <![CDATA[
+ var noCancel = true;
+ // handle any xml attribute event handlers
+ var handler = this.getAttribute("on"+aEventType);
+ if (handler) {
+ var fn = new Function("eventParam", "domEvent", handler);
+ var returned = fn.apply(this, [aEventParam, aTriggeringEvent]);
+ if (returned == false)
+ noCancel = false;
+ }
+
+ return noCancel;
+ ]]>
+ </body>
+ </method>
+
+ <!-- =================== TREE VIEW =================== -->
+
+ <field name="view"><![CDATA[
+ ({
+ mTextbox: this,
+ mTree: null,
+ mSelection: null,
+ mRowCount: 0,
+
+ clearResults: function()
+ {
+ var oldCount = this.mRowCount;
+ this.mRowCount = 0;
+
+ if (this.mTree) {
+ this.mTree.rowCountChanged(0, -oldCount);
+ this.mTree.scrollToRow(0);
+ }
+ },
+
+ updateResults: function(aRow, aCount)
+ {
+ this.mRowCount += aCount;
+
+ if (this.mTree)
+ this.mTree.rowCountChanged(aRow, aCount);
+ },
+
+ //////////////////////////////////////////////////////////
+ // nsIAutoCompleteController interface
+
+ // this is the only method required by the treebody mouseup handler
+ handleEnter: function(aIsPopupSelection) {
+ this.mTextbox.onResultClick();
+ },
+
+ //////////////////////////////////////////////////////////
+ // nsITreeView interface
+
+ get rowCount() {
+ return this.mRowCount;
+ },
+
+ get selection() {
+ return this.mSelection;
+ },
+
+ set selection(aVal) {
+ return this.mSelection = aVal;
+ },
+
+ setTree: function(aTree)
+ {
+ this.mTree = aTree;
+ },
+
+ getCellText: function(aRow, aCol)
+ {
+ for (var name in this.mTextbox.mSessions) {
+ if (aRow < this.mTextbox.mLastRows[name]) {
+ var result = this.mTextbox.mLastResults[name];
+ switch (aCol.id) {
+ case "treecolAutoCompleteValue":
+ return result.errorDescription || result.getLabelAt(aRow);
+ case "treecolAutoCompleteComment":
+ if (!result.errorDescription)
+ return result.getCommentAt(aRow);
+ default:
+ return "";
+ }
+ }
+ aRow -= this.mTextbox.mLastRows[name];
+ }
+ return "";
+ },
+
+ getRowProperties: function(aIndex)
+ {
+ return "";
+ },
+
+ getCellProperties: function(aIndex, aCol)
+ {
+ // for the value column, append nsIAutoCompleteItem::className
+ // to the property list so that we can style this column
+ // using that property
+ if (aCol.id == "treecolAutoCompleteValue") {
+ for (var name in this.mTextbox.mSessions) {
+ if (aIndex < this.mTextbox.mLastRows[name]) {
+ var result = this.mTextbox.mLastResults[name];
+ if (result.errorDescription)
+ return "";
+ return result.getStyleAt(aIndex);
+ }
+ aIndex -= this.mTextbox.mLastRows[name];
+ }
+ }
+ return "";
+ },
+
+ getColumnProperties: function(aCol)
+ {
+ return "";
+ },
+
+ getImageSrc: function(aRow, aCol)
+ {
+ if (aCol.id == "treecolAutoCompleteValue") {
+ for (var name in this.mTextbox.mSessions) {
+ if (aRow < this.mTextbox.mLastRows[name]) {
+ var result = this.mTextbox.mLastResults[name];
+ if (result.errorDescription)
+ return "";
+ return result.getImageAt(aRow);
+ }
+ aRow -= this.mTextbox.mLastRows[name];
+ }
+ }
+ return "";
+ },
+
+ getParentIndex: function(aRowIndex) { },
+ hasNextSibling: function(aRowIndex, aAfterIndex) { },
+ getLevel: function(aIndex) {},
+ getProgressMode: function(aRow, aCol) {},
+ getCellValue: function(aRow, aCol) {},
+ isContainer: function(aIndex) {},
+ isContainerOpen: function(aIndex) {},
+ isContainerEmpty: function(aIndex) {},
+ isSeparator: function(aIndex) {},
+ isSorted: function() {},
+ toggleOpenState: function(aIndex) {},
+ selectionChanged: function() {},
+ cycleHeader: function(aCol) {},
+ cycleCell: function(aRow, aCol) {},
+ isEditable: function(aRow, aCol) {},
+ isSelectable: function(aRow, aCol) {},
+ setCellValue: function(aRow, aCol, aValue) {},
+ setCellText: function(aRow, aCol, aValue) {},
+ performAction: function(aAction) {},
+ performActionOnRow: function(aAction, aRow) {},
+ performActionOnCell: function(aAction, aRow, aCol) {}
+ });
+ ]]></field>
+
+ </implementation>
+
+ <handlers>
+ <handler event="input"
+ action="if (!this.ignoreInputEvent) this.processInput();"/>
+
+ <handler event="keypress" phase="capturing"
+ action="return this.processKeyPress(event);"/>
+
+ <handler event="compositionstart" phase="capturing"
+ action="this.processStartComposition();"/>
+
+ <handler event="focus" phase="capturing"
+ action="this.userAction = 'typing';"/>
+
+ <handler event="blur" phase="capturing"
+ action="if ( !(this.ignoreBlurWhileSearching &amp;&amp; this.isSearching) ) {this.userAction = 'none'; this.finishAutoComplete(false, false, event);}"/>
+
+ <handler event="mousedown" phase="capturing"
+ action="if ( !this.mMenuOpen ) this.finishAutoComplete(false, false, event);"/>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/popup.xml#popup">
+ <resources>
+ <stylesheet src="chrome://communicator/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content ignorekeys="true" level="top">
+ <xul:tree anonid="tree" class="autocomplete-tree plain" flex="1">
+ <xul:treecols anonid="treecols">
+ <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteValue" flex="2"/>
+ <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteComment" flex="1" hidden="true"/>
+ </xul:treecols>
+ <xul:treechildren anonid="treebody" class="autocomplete-treebody"/>
+ </xul:tree>
+ </content>
+
+ <implementation implements="nsIAutoCompletePopup">
+ <constructor><![CDATA[
+ if (this.textbox && this.textbox.view)
+ this.initialize();
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ if (this.view)
+ this.tree.view = null;
+ ]]></destructor>
+
+ <field name="textbox">
+ document.getBindingParent(this);
+ </field>
+
+ <field name="tree">
+ document.getAnonymousElementByAttribute(this, "anonid", "tree");
+ </field>
+
+ <field name="treecols">
+ document.getAnonymousElementByAttribute(this, "anonid", "treecols");
+ </field>
+
+ <field name="treebody">
+ document.getAnonymousElementByAttribute(this, "anonid", "treebody");
+ </field>
+
+ <field name="view">
+ null
+ </field>
+
+ <!-- Setting tree.view doesn't always immediately create a selection,
+ so we ensure the selection by asking the tree for the view. Note:
+ this.view.selection is quicker if we know the selection exists. -->
+ <property name="selection" onget="return this.tree.view.selection;"/>
+
+ <property name="pageCount"
+ onget="return this.tree.treeBoxObject.getPageLength();"/>
+
+ <field name="maxRows">0</field>
+ <field name="mLastRows">0</field>
+
+ <method name="initialize">
+ <body><![CDATA[
+ this.showCommentColumn = this.textbox.showCommentColumn;
+ this.tree.view = this.textbox.view;
+ this.view = this.textbox.view;
+ this.maxRows = this.textbox.maxRows;
+ ]]></body>
+ </method>
+
+ <property name="showCommentColumn"
+ onget="return !this.treecols.lastChild.hidden;"
+ onset="this.treecols.lastChild.hidden = !val; return val;"/>
+
+ <method name="adjustHeight">
+ <body><![CDATA[
+ // detect the desired height of the tree
+ var bx = this.tree.treeBoxObject;
+ var view = this.view;
+ var rows = this.maxRows || 6;
+ if (!view.rowCount || (rows && view.rowCount < rows))
+ rows = view.rowCount;
+
+ var height = rows * bx.rowHeight;
+
+ if (height == 0)
+ this.tree.setAttribute("collapsed", "true");
+ else {
+ if (this.tree.hasAttribute("collapsed"))
+ this.tree.removeAttribute("collapsed");
+ this.tree.setAttribute("height", height);
+ }
+ ]]></body>
+ </method>
+
+ <method name="clearSelection">
+ <body>
+ this.selection.clearSelection();
+ </body>
+ </method>
+
+ <method name="getNextIndex">
+ <parameter name="aReverse"/>
+ <parameter name="aPage"/>
+ <parameter name="aIndex"/>
+ <parameter name="aMaxRow"/>
+ <body><![CDATA[
+ if (aMaxRow < 0)
+ return -1;
+
+ if (aIndex == -1)
+ return aReverse ? aMaxRow : 0;
+ if (aIndex == (aReverse ? 0 : aMaxRow))
+ return -1;
+
+ var amount = aPage ? this.pageCount - 1 : 1;
+ aIndex = aReverse ? aIndex - amount : aIndex + amount;
+ if (aIndex > aMaxRow)
+ return aMaxRow;
+ if (aIndex < 0)
+ return 0;
+ return aIndex;
+ ]]></body>
+ </method>
+
+ <!-- =================== nsIAutoCompletePopup =================== -->
+
+ <field name="input">
+ null
+ </field>
+
+ <!-- This property is meant to be overriden by bindings extending
+ this one. When the user selects an item from the list by
+ hitting enter or clicking, this method can set the value
+ of the textbox to a different value if it wants to. -->
+ <property name="overrideValue" readonly="true" onget="return null;"/>
+
+ <property name="selectedIndex">
+ <getter>
+ if (!this.view || !this.selection.count)
+ return -1;
+ var start = {}, end = {};
+ this.view.selection.getRangeAt(0, start, end);
+ return start.value;
+ </getter>
+ <setter>
+ if (this.view) {
+ this.selection.select(val);
+ if (val >= 0) {
+ this.view.selection.currentIndex = -1;
+ this.tree.treeBoxObject.ensureRowIsVisible(val);
+ }
+ }
+ return val;
+ </setter>
+ </property>
+
+ <property name="popupOpen" onget="return !!this.input;" readonly="true"/>
+
+ <method name="openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body><![CDATA[
+ if (!this.input) {
+ this.tree.view = aInput.controller;
+ this.view = this.tree.view;
+ this.showCommentColumn = aInput.showCommentColumn;
+ this.maxRows = aInput.maxRows;
+ this.invalidate();
+
+ var viewer = aElement
+ .ownerDocument
+ .defaultView
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShell)
+ .contentViewer;
+ var rect = aElement.getBoundingClientRect();
+ var width = Math.round((rect.right - rect.left) * viewer.fullZoom);
+ this.setAttribute("width", width > 100 ? width : 100);
+ // Adjust the direction (which is not inherited) of the autocomplete
+ // popup list, based on the textbox direction. (Bug 707039)
+ this.style.direction = aElement.ownerDocument.defaultView
+ .getComputedStyle(aElement)
+ .direction;
+ this.popupBoxObject.setConsumeRollupEvent(aInput.consumeRollupEvent
+ ? PopupBoxObject.ROLLUP_CONSUME
+ : PopupBoxObject.ROLLUP_NO_CONSUME);
+ this.openPopup(aElement, "after_start", 0, 0, false, false);
+ if (this.state != "closed")
+ this.input = aInput;
+ }
+ ]]></body>
+ </method>
+
+ <method name="closePopup">
+ <body>
+ this.hidePopup();
+ </body>
+ </method>
+
+ <method name="invalidate">
+ <body>
+ if (this.view)
+ this.adjustHeight();
+ this.tree.treeBoxObject.invalidate();
+ </body>
+ </method>
+
+ <method name="selectBy">
+ <parameter name="aReverse"/>
+ <parameter name="aPage"/>
+ <body><![CDATA[
+ try {
+ return this.selectedIndex = this.getNextIndex(aReverse, aPage, this.selectedIndex, this.view.rowCount - 1);
+ } catch (ex) {
+ // do nothing - occasionally timer-related js errors happen here
+ // e.g. "this.selectedIndex has no properties", when you type fast and hit a
+ // navigation key before this popup has opened
+ return -1;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing">
+ if (this.textbox)
+ this.textbox.mMenuOpen = true;
+ </handler>
+
+ <handler event="popuphiding">
+ if (this.textbox)
+ this.textbox.mMenuOpen = false;
+ this.clearSelection();
+ this.input = null;
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-treebody">
+ <implementation>
+ <field name="popup">document.getBindingParent(this);</field>
+
+ <field name="mLastMoveTime">Date.now()</field>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseout" action="this.popup.selectedIndex = -1;"/>
+
+ <handler event="mouseup"><![CDATA[
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != -1) {
+ this.popup.selectedIndex = rc;
+ this.popup.view.handleEnter(true);
+ }
+ ]]></handler>
+
+ <handler event="mousemove"><![CDATA[
+ if (Date.now() - this.mLastMoveTime > 30) {
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != -1 && rc != this.popup.selectedIndex)
+ this.popup.selectedIndex = rc;
+ this.mLastMoveTime = Date.now();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-history-popup"
+ extends="chrome://global/content/bindings/popup.xml#popup-scrollbars">
+ <resources>
+ <stylesheet src="chrome://communicator/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <implementation>
+ <method name="removeOpenAttribute">
+ <parameter name="parentNode"/>
+ <body><![CDATA[
+ parentNode.removeAttribute("open");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popuphiding"><![CDATA[
+ setTimeout(this.removeOpenAttribute, 0, this.parentNode);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker">
+
+ <implementation>
+ <method name="showPopup">
+ <body><![CDATA[
+ var textbox = document.getBindingParent(this);
+ var kids = textbox.getElementsByClassName("autocomplete-history-popup");
+ if (kids.item(0) && textbox.getAttribute("open") != "true") { // Open history popup
+ var w = textbox.boxObject.width;
+ if (w != kids[0].boxObject.width)
+ kids[0].width = w;
+ kids[0].showPopup(textbox, -1, -1, "popup", "bottomleft", "topleft");
+ textbox.setAttribute("open", "true");
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown"><![CDATA[
+ this.showPopup();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+</bindings>