diff options
Diffstat (limited to 'xpfe/components/autocomplete/resources/content/autocomplete.xml')
-rw-r--r-- | xpfe/components/autocomplete/resources/content/autocomplete.xml | 1646 |
1 files changed, 1646 insertions, 0 deletions
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 && 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> |