Revision control
Copy as Markdown
Other Tools
<?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
<bindings id="autocompleteBindings"
<binding id="autocomplete" role="xul:combobox"
<resources>
</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">
<children/>
<html:input anonid="input" class="autocomplete-textbox textbox-input"
allowevents="true"
xbl:inherits="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[
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 Cc) {
try {
this.mSessions[name] =
Cc[contractid].getService(Ci.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: Ci.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[
Cc["@mozilla.org/preferences-service;1"]
.getService(Ci.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 = Ci.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) {},
});
]]></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 && 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>
<resources>
</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.ownerGlobal
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.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
this.style.direction = aElement.ownerGlobal
.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"
<resources>
</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>
<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>