Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | <?xml version="1.0"?> |
michael@0 | 2 | |
michael@0 | 3 | # -*- Mode: HTML -*- |
michael@0 | 4 | # This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 5 | # License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 6 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 7 | |
michael@0 | 8 | <!DOCTYPE bindings [ |
michael@0 | 9 | <!ENTITY % actionsDTD SYSTEM "chrome://global/locale/actions.dtd"> |
michael@0 | 10 | %actionsDTD; |
michael@0 | 11 | ]> |
michael@0 | 12 | |
michael@0 | 13 | <bindings id="autocompleteBindings" |
michael@0 | 14 | xmlns="http://www.mozilla.org/xbl" |
michael@0 | 15 | xmlns:html="http://www.w3.org/1999/xhtml" |
michael@0 | 16 | xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" |
michael@0 | 17 | xmlns:xbl="http://www.mozilla.org/xbl"> |
michael@0 | 18 | |
michael@0 | 19 | <binding id="autocomplete" role="xul:combobox" |
michael@0 | 20 | extends="chrome://global/content/bindings/textbox.xml#textbox"> |
michael@0 | 21 | <resources> |
michael@0 | 22 | <stylesheet src="chrome://global/skin/autocomplete.css"/> |
michael@0 | 23 | </resources> |
michael@0 | 24 | |
michael@0 | 25 | <content sizetopopup="pref"> |
michael@0 | 26 | <xul:hbox class="autocomplete-textbox-container" flex="1" xbl:inherits="focused"> |
michael@0 | 27 | <children includes="image|deck|stack|box"> |
michael@0 | 28 | <xul:image class="autocomplete-icon" allowevents="true"/> |
michael@0 | 29 | </children> |
michael@0 | 30 | |
michael@0 | 31 | <xul:hbox anonid="textbox-input-box" class="textbox-input-box" flex="1" xbl:inherits="tooltiptext=inputtooltiptext"> |
michael@0 | 32 | <children/> |
michael@0 | 33 | <html:input anonid="input" class="autocomplete-textbox textbox-input" |
michael@0 | 34 | allowevents="true" |
michael@0 | 35 | xbl:inherits="tooltiptext=inputtooltiptext,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/> |
michael@0 | 36 | </xul:hbox> |
michael@0 | 37 | <children includes="hbox"/> |
michael@0 | 38 | </xul:hbox> |
michael@0 | 39 | |
michael@0 | 40 | <xul:dropmarker anonid="historydropmarker" class="autocomplete-history-dropmarker" |
michael@0 | 41 | allowevents="true" |
michael@0 | 42 | xbl:inherits="open,enablehistory,parentfocused=focused"/> |
michael@0 | 43 | |
michael@0 | 44 | <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/> |
michael@0 | 45 | |
michael@0 | 46 | <children includes="toolbarbutton"/> |
michael@0 | 47 | </content> |
michael@0 | 48 | |
michael@0 | 49 | <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement"> |
michael@0 | 50 | <field name="mController">null</field> |
michael@0 | 51 | <field name="mSearchNames">null</field> |
michael@0 | 52 | <field name="mIgnoreInput">false</field> |
michael@0 | 53 | <field name="mEnterEvent">null</field> |
michael@0 | 54 | |
michael@0 | 55 | <field name="_searchBeginHandler">null</field> |
michael@0 | 56 | <field name="_searchCompleteHandler">null</field> |
michael@0 | 57 | <field name="_textEnteredHandler">null</field> |
michael@0 | 58 | <field name="_textRevertedHandler">null</field> |
michael@0 | 59 | |
michael@0 | 60 | <constructor><![CDATA[ |
michael@0 | 61 | this.mController = Components.classes["@mozilla.org/autocomplete/controller;1"]. |
michael@0 | 62 | getService(Components.interfaces.nsIAutoCompleteController); |
michael@0 | 63 | |
michael@0 | 64 | this._searchBeginHandler = this.initEventHandler("searchbegin"); |
michael@0 | 65 | this._searchCompleteHandler = this.initEventHandler("searchcomplete"); |
michael@0 | 66 | this._textEnteredHandler = this.initEventHandler("textentered"); |
michael@0 | 67 | this._textRevertedHandler = this.initEventHandler("textreverted"); |
michael@0 | 68 | |
michael@0 | 69 | // For security reasons delay searches on pasted values. |
michael@0 | 70 | this.inputField.controllers.insertControllerAt(0, this._pasteController); |
michael@0 | 71 | ]]></constructor> |
michael@0 | 72 | |
michael@0 | 73 | <destructor><![CDATA[ |
michael@0 | 74 | this.inputField.controllers.removeController(this._pasteController); |
michael@0 | 75 | ]]></destructor> |
michael@0 | 76 | |
michael@0 | 77 | <!-- =================== nsIAutoCompleteInput =================== --> |
michael@0 | 78 | |
michael@0 | 79 | <field name="popup"><![CDATA[ |
michael@0 | 80 | // Wrap in a block so that the let statements don't |
michael@0 | 81 | // create properties on 'this' (bug 635252). |
michael@0 | 82 | { |
michael@0 | 83 | let popup = null; |
michael@0 | 84 | let popupId = this.getAttribute("autocompletepopup"); |
michael@0 | 85 | if (popupId) |
michael@0 | 86 | popup = document.getElementById(popupId); |
michael@0 | 87 | if (!popup) { |
michael@0 | 88 | popup = document.createElement("panel"); |
michael@0 | 89 | popup.setAttribute("type", "autocomplete"); |
michael@0 | 90 | popup.setAttribute("noautofocus", "true"); |
michael@0 | 91 | |
michael@0 | 92 | let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset"); |
michael@0 | 93 | popupset.appendChild(popup); |
michael@0 | 94 | } |
michael@0 | 95 | popup.mInput = this; |
michael@0 | 96 | popup; |
michael@0 | 97 | } |
michael@0 | 98 | ]]></field> |
michael@0 | 99 | |
michael@0 | 100 | <property name="controller" onget="return this.mController;" readonly="true"/> |
michael@0 | 101 | |
michael@0 | 102 | <property name="popupOpen" |
michael@0 | 103 | onget="return this.popup.popupOpen;" |
michael@0 | 104 | onset="if (val) this.openPopup(); else this.closePopup();"/> |
michael@0 | 105 | |
michael@0 | 106 | <property name="disableAutoComplete" |
michael@0 | 107 | onset="this.setAttribute('disableautocomplete', val); return val;" |
michael@0 | 108 | onget="return this.getAttribute('disableautocomplete') == 'true';"/> |
michael@0 | 109 | |
michael@0 | 110 | <property name="completeDefaultIndex" |
michael@0 | 111 | onset="this.setAttribute('completedefaultindex', val); return val;" |
michael@0 | 112 | onget="return this.getAttribute('completedefaultindex') == 'true';"/> |
michael@0 | 113 | |
michael@0 | 114 | <property name="completeSelectedIndex" |
michael@0 | 115 | onset="this.setAttribute('completeselectedindex', val); return val;" |
michael@0 | 116 | onget="return this.getAttribute('completeselectedindex') == 'true';"/> |
michael@0 | 117 | |
michael@0 | 118 | <property name="forceComplete" |
michael@0 | 119 | onset="this.setAttribute('forcecomplete', val); return val;" |
michael@0 | 120 | onget="return this.getAttribute('forcecomplete') == 'true';"/> |
michael@0 | 121 | |
michael@0 | 122 | <property name="minResultsForPopup" |
michael@0 | 123 | onset="this.setAttribute('minresultsforpopup', val); return val;" |
michael@0 | 124 | onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/> |
michael@0 | 125 | |
michael@0 | 126 | <property name="showCommentColumn" |
michael@0 | 127 | onset="this.setAttribute('showcommentcolumn', val); return val;" |
michael@0 | 128 | onget="return this.getAttribute('showcommentcolumn') == 'true';"/> |
michael@0 | 129 | |
michael@0 | 130 | <property name="showImageColumn" |
michael@0 | 131 | onset="this.setAttribute('showimagecolumn', val); return val;" |
michael@0 | 132 | onget="return this.getAttribute('showimagecolumn') == 'true';"/> |
michael@0 | 133 | |
michael@0 | 134 | <property name="timeout" |
michael@0 | 135 | onset="this.setAttribute('timeout', val); return val;"> |
michael@0 | 136 | <getter><![CDATA[ |
michael@0 | 137 | // For security reasons delay searches on pasted values. |
michael@0 | 138 | if (this._valueIsPasted) { |
michael@0 | 139 | let t = parseInt(this.getAttribute('pastetimeout')); |
michael@0 | 140 | return isNaN(t) ? 1000 : t; |
michael@0 | 141 | } |
michael@0 | 142 | |
michael@0 | 143 | let t = parseInt(this.getAttribute('timeout')); |
michael@0 | 144 | return isNaN(t) ? 50 : t; |
michael@0 | 145 | ]]></getter> |
michael@0 | 146 | </property> |
michael@0 | 147 | |
michael@0 | 148 | <property name="searchParam" |
michael@0 | 149 | onget="return this.getAttribute('autocompletesearchparam') || '';" |
michael@0 | 150 | onset="this.setAttribute('autocompletesearchparam', val); return val;"/> |
michael@0 | 151 | |
michael@0 | 152 | <property name="searchCount" readonly="true" |
michael@0 | 153 | onget="this.initSearchNames(); return this.mSearchNames.length;"/> |
michael@0 | 154 | |
michael@0 | 155 | <field name="PrivateBrowsingUtils" readonly="true"> |
michael@0 | 156 | let utils = {}; |
michael@0 | 157 | Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", utils); |
michael@0 | 158 | utils.PrivateBrowsingUtils |
michael@0 | 159 | </field> |
michael@0 | 160 | |
michael@0 | 161 | <property name="inPrivateContext" readonly="true" |
michael@0 | 162 | onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/> |
michael@0 | 163 | |
michael@0 | 164 | <!-- This is the maximum number of drop-down rows we get when we |
michael@0 | 165 | hit the drop marker beside fields that have it (like the URLbar).--> |
michael@0 | 166 | <field name="maxDropMarkerRows" readonly="true">14</field> |
michael@0 | 167 | |
michael@0 | 168 | <method name="getSearchAt"> |
michael@0 | 169 | <parameter name="aIndex"/> |
michael@0 | 170 | <body><![CDATA[ |
michael@0 | 171 | this.initSearchNames(); |
michael@0 | 172 | return this.mSearchNames[aIndex]; |
michael@0 | 173 | ]]></body> |
michael@0 | 174 | </method> |
michael@0 | 175 | |
michael@0 | 176 | <property name="textValue" |
michael@0 | 177 | onget="return this.value;"> |
michael@0 | 178 | <setter><![CDATA[ |
michael@0 | 179 | // Completing a result should simulate the user typing the result, |
michael@0 | 180 | // so fire an input event. |
michael@0 | 181 | // Trim popup selected values, but never trim results coming from |
michael@0 | 182 | // autofill. |
michael@0 | 183 | if (this.popup.selectedIndex == -1) |
michael@0 | 184 | this._disableTrim = true; |
michael@0 | 185 | this.value = val; |
michael@0 | 186 | this._disableTrim = false; |
michael@0 | 187 | |
michael@0 | 188 | var evt = document.createEvent("UIEvents"); |
michael@0 | 189 | evt.initUIEvent("input", true, false, window, 0); |
michael@0 | 190 | this.mIgnoreInput = true; |
michael@0 | 191 | this.dispatchEvent(evt); |
michael@0 | 192 | this.mIgnoreInput = false; |
michael@0 | 193 | return this.value; |
michael@0 | 194 | ]]></setter> |
michael@0 | 195 | </property> |
michael@0 | 196 | |
michael@0 | 197 | <method name="selectTextRange"> |
michael@0 | 198 | <parameter name="aStartIndex"/> |
michael@0 | 199 | <parameter name="aEndIndex"/> |
michael@0 | 200 | <body><![CDATA[ |
michael@0 | 201 | this.inputField.setSelectionRange(aStartIndex, aEndIndex); |
michael@0 | 202 | ]]></body> |
michael@0 | 203 | </method> |
michael@0 | 204 | |
michael@0 | 205 | <method name="onSearchBegin"> |
michael@0 | 206 | <body><![CDATA[ |
michael@0 | 207 | if (this._searchBeginHandler) |
michael@0 | 208 | this._searchBeginHandler(); |
michael@0 | 209 | ]]></body> |
michael@0 | 210 | </method> |
michael@0 | 211 | |
michael@0 | 212 | <method name="onSearchComplete"> |
michael@0 | 213 | <body><![CDATA[ |
michael@0 | 214 | if (this.mController.matchCount == 0) |
michael@0 | 215 | this.setAttribute("nomatch", "true"); |
michael@0 | 216 | else |
michael@0 | 217 | this.removeAttribute("nomatch"); |
michael@0 | 218 | |
michael@0 | 219 | if (this._searchCompleteHandler) |
michael@0 | 220 | this._searchCompleteHandler(); |
michael@0 | 221 | ]]></body> |
michael@0 | 222 | </method> |
michael@0 | 223 | |
michael@0 | 224 | <method name="onTextEntered"> |
michael@0 | 225 | <body><![CDATA[ |
michael@0 | 226 | let rv = false; |
michael@0 | 227 | if (this._textEnteredHandler) |
michael@0 | 228 | rv = this._textEnteredHandler(this.mEnterEvent); |
michael@0 | 229 | this.mEnterEvent = null; |
michael@0 | 230 | return rv; |
michael@0 | 231 | ]]></body> |
michael@0 | 232 | </method> |
michael@0 | 233 | |
michael@0 | 234 | <method name="onTextReverted"> |
michael@0 | 235 | <body><![CDATA[ |
michael@0 | 236 | if (this._textRevertedHandler) |
michael@0 | 237 | return this._textRevertedHandler(); |
michael@0 | 238 | return false; |
michael@0 | 239 | ]]></body> |
michael@0 | 240 | </method> |
michael@0 | 241 | |
michael@0 | 242 | <!-- =================== nsIDOMXULMenuListElement =================== --> |
michael@0 | 243 | |
michael@0 | 244 | <property name="editable" readonly="true" |
michael@0 | 245 | onget="return true;" /> |
michael@0 | 246 | |
michael@0 | 247 | <property name="crop" |
michael@0 | 248 | onset="this.setAttribute('crop',val); return val;" |
michael@0 | 249 | onget="return this.getAttribute('crop');"/> |
michael@0 | 250 | |
michael@0 | 251 | <property name="open" |
michael@0 | 252 | onget="return this.getAttribute('open') == 'true';"> |
michael@0 | 253 | <setter><![CDATA[ |
michael@0 | 254 | if (val) |
michael@0 | 255 | this.showHistoryPopup(); |
michael@0 | 256 | else |
michael@0 | 257 | this.closePopup(); |
michael@0 | 258 | ]]></setter> |
michael@0 | 259 | </property> |
michael@0 | 260 | |
michael@0 | 261 | <!-- =================== PUBLIC MEMBERS =================== --> |
michael@0 | 262 | |
michael@0 | 263 | <field name="valueIsTyped">false</field> |
michael@0 | 264 | <field name="_disableTrim">false</field> |
michael@0 | 265 | <property name="value"> |
michael@0 | 266 | <getter><![CDATA[ |
michael@0 | 267 | if (typeof this.onBeforeValueGet == "function") { |
michael@0 | 268 | var result = this.onBeforeValueGet(); |
michael@0 | 269 | if (result) |
michael@0 | 270 | return result.value; |
michael@0 | 271 | } |
michael@0 | 272 | return this.inputField.value; |
michael@0 | 273 | ]]></getter> |
michael@0 | 274 | <setter><![CDATA[ |
michael@0 | 275 | this.mIgnoreInput = true; |
michael@0 | 276 | |
michael@0 | 277 | if (typeof this.onBeforeValueSet == "function") |
michael@0 | 278 | val = this.onBeforeValueSet(val); |
michael@0 | 279 | |
michael@0 | 280 | if (typeof this.trimValue == "function" && !this._disableTrim) |
michael@0 | 281 | val = this.trimValue(val); |
michael@0 | 282 | |
michael@0 | 283 | this.valueIsTyped = false; |
michael@0 | 284 | this.inputField.value = val; |
michael@0 | 285 | |
michael@0 | 286 | if (typeof this.formatValue == "function") |
michael@0 | 287 | this.formatValue(); |
michael@0 | 288 | |
michael@0 | 289 | this.mIgnoreInput = false; |
michael@0 | 290 | var event = document.createEvent('Events'); |
michael@0 | 291 | event.initEvent('ValueChange', true, true); |
michael@0 | 292 | this.inputField.dispatchEvent(event); |
michael@0 | 293 | return val; |
michael@0 | 294 | ]]></setter> |
michael@0 | 295 | </property> |
michael@0 | 296 | |
michael@0 | 297 | <property name="focused" readonly="true" |
michael@0 | 298 | onget="return this.getAttribute('focused') == 'true';"/> |
michael@0 | 299 | |
michael@0 | 300 | <!-- maximum number of rows to display at a time --> |
michael@0 | 301 | <property name="maxRows" |
michael@0 | 302 | onset="this.setAttribute('maxrows', val); return val;" |
michael@0 | 303 | onget="return parseInt(this.getAttribute('maxrows')) || 0;"/> |
michael@0 | 304 | |
michael@0 | 305 | <!-- option to allow scrolling through the list via the tab key, rather than |
michael@0 | 306 | tab moving focus out of the textbox --> |
michael@0 | 307 | <property name="tabScrolling" |
michael@0 | 308 | onset="return this.setAttribute('tabscrolling', val); return val;" |
michael@0 | 309 | onget="return this.getAttribute('tabscrolling') == 'true';"/> |
michael@0 | 310 | |
michael@0 | 311 | <!-- disable key navigation handling in the popup results --> |
michael@0 | 312 | <property name="disableKeyNavigation" |
michael@0 | 313 | onset="this.setAttribute('disablekeynavigation', val); return val;" |
michael@0 | 314 | onget="return this.getAttribute('disablekeynavigation') == 'true';"/> |
michael@0 | 315 | |
michael@0 | 316 | <!-- option to completely ignore any blur events while |
michael@0 | 317 | searches are still going on. This is useful so that nothing |
michael@0 | 318 | gets autopicked if the window is required to lose focus for |
michael@0 | 319 | some reason (eg in LDAP autocomplete, another window may be |
michael@0 | 320 | brought up so that the user can enter a password to authenticate |
michael@0 | 321 | to an LDAP server). --> |
michael@0 | 322 | <property name="ignoreBlurWhileSearching" |
michael@0 | 323 | onset="this.setAttribute('ignoreblurwhilesearching', val); return val;" |
michael@0 | 324 | onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/> |
michael@0 | 325 | |
michael@0 | 326 | <!-- option to highlight entries that don't have any matches --> |
michael@0 | 327 | <property name="highlightNonMatches" |
michael@0 | 328 | onset="this.setAttribute('highlightnonmatches', val); return val;" |
michael@0 | 329 | onget="return this.getAttribute('highlightnonmatches') == 'true';"/> |
michael@0 | 330 | |
michael@0 | 331 | <!-- =================== PRIVATE MEMBERS =================== --> |
michael@0 | 332 | |
michael@0 | 333 | <!-- ::::::::::::: autocomplete controller ::::::::::::: --> |
michael@0 | 334 | |
michael@0 | 335 | <method name="attachController"> |
michael@0 | 336 | <body><![CDATA[ |
michael@0 | 337 | this.mController.input = this; |
michael@0 | 338 | ]]></body> |
michael@0 | 339 | </method> |
michael@0 | 340 | |
michael@0 | 341 | <method name="detachController"> |
michael@0 | 342 | <body><![CDATA[ |
michael@0 | 343 | try { |
michael@0 | 344 | if (this.mController.input == this) |
michael@0 | 345 | this.mController.input = null; |
michael@0 | 346 | } catch (ex) { |
michael@0 | 347 | // nothing really to do. |
michael@0 | 348 | } |
michael@0 | 349 | ]]></body> |
michael@0 | 350 | </method> |
michael@0 | 351 | |
michael@0 | 352 | <!-- ::::::::::::: popup opening ::::::::::::: --> |
michael@0 | 353 | |
michael@0 | 354 | <method name="openPopup"> |
michael@0 | 355 | <body><![CDATA[ |
michael@0 | 356 | this.popup.openAutocompletePopup(this, this); |
michael@0 | 357 | ]]></body> |
michael@0 | 358 | </method> |
michael@0 | 359 | |
michael@0 | 360 | <method name="closePopup"> |
michael@0 | 361 | <body><![CDATA[ |
michael@0 | 362 | this.popup.setAttribute("consumeoutsideclicks", "false"); |
michael@0 | 363 | this.popup.closePopup(); |
michael@0 | 364 | ]]></body> |
michael@0 | 365 | </method> |
michael@0 | 366 | |
michael@0 | 367 | <method name="showHistoryPopup"> |
michael@0 | 368 | <body><![CDATA[ |
michael@0 | 369 | // history dropmarker pushed state |
michael@0 | 370 | function cleanup(popup) { |
michael@0 | 371 | popup.removeEventListener("popupshowing", onShow, false); |
michael@0 | 372 | } |
michael@0 | 373 | function onShow(event) { |
michael@0 | 374 | var popup = event.target, input = popup.input; |
michael@0 | 375 | cleanup(popup); |
michael@0 | 376 | input.setAttribute("open", "true"); |
michael@0 | 377 | function onHide() { |
michael@0 | 378 | input.removeAttribute("open"); |
michael@0 | 379 | popup.setAttribute("consumeoutsideclicks", "false"); |
michael@0 | 380 | popup.removeEventListener("popuphiding", onHide, false); |
michael@0 | 381 | } |
michael@0 | 382 | popup.addEventListener("popuphiding", onHide, false); |
michael@0 | 383 | } |
michael@0 | 384 | this.popup.addEventListener("popupshowing", onShow, false); |
michael@0 | 385 | setTimeout(cleanup, 1000, this.popup); |
michael@0 | 386 | |
michael@0 | 387 | // Store our "normal" maxRows on the popup, so that it can reset the |
michael@0 | 388 | // value when the popup is hidden. |
michael@0 | 389 | this.popup._normalMaxRows = this.maxRows; |
michael@0 | 390 | |
michael@0 | 391 | // Increase our maxRows temporarily, since we want the dropdown to |
michael@0 | 392 | // be bigger in this case. The popup's popupshowing/popuphiding |
michael@0 | 393 | // handlers will take care of resetting this. |
michael@0 | 394 | this.maxRows = this.maxDropMarkerRows; |
michael@0 | 395 | |
michael@0 | 396 | // Ensure that we have focus. |
michael@0 | 397 | if (!this.focused) |
michael@0 | 398 | this.focus(); |
michael@0 | 399 | this.popup.setAttribute("consumeoutsideclicks", "true"); |
michael@0 | 400 | this.attachController(); |
michael@0 | 401 | this.mController.startSearch(""); |
michael@0 | 402 | ]]></body> |
michael@0 | 403 | </method> |
michael@0 | 404 | |
michael@0 | 405 | <method name="toggleHistoryPopup"> |
michael@0 | 406 | <body><![CDATA[ |
michael@0 | 407 | if (!this.popup.popupOpen) |
michael@0 | 408 | this.showHistoryPopup(); |
michael@0 | 409 | else |
michael@0 | 410 | this.closePopup(); |
michael@0 | 411 | ]]></body> |
michael@0 | 412 | </method> |
michael@0 | 413 | |
michael@0 | 414 | <!-- ::::::::::::: event dispatching ::::::::::::: --> |
michael@0 | 415 | |
michael@0 | 416 | <method name="initEventHandler"> |
michael@0 | 417 | <parameter name="aEventType"/> |
michael@0 | 418 | <body><![CDATA[ |
michael@0 | 419 | let handlerString = this.getAttribute("on" + aEventType); |
michael@0 | 420 | if (handlerString) { |
michael@0 | 421 | return (new Function("eventType", "param", handlerString)).bind(this, aEventType); |
michael@0 | 422 | } |
michael@0 | 423 | return null; |
michael@0 | 424 | ]]></body> |
michael@0 | 425 | </method> |
michael@0 | 426 | |
michael@0 | 427 | <!-- ::::::::::::: key handling ::::::::::::: --> |
michael@0 | 428 | |
michael@0 | 429 | <method name="onKeyPress"> |
michael@0 | 430 | <parameter name="aEvent"/> |
michael@0 | 431 | <body><![CDATA[ |
michael@0 | 432 | if (aEvent.target.localName != "textbox") |
michael@0 | 433 | return true; // Let child buttons of autocomplete take input |
michael@0 | 434 | |
michael@0 | 435 | //XXXpch this is so bogus... |
michael@0 | 436 | if (aEvent.defaultPrevented) |
michael@0 | 437 | return false; |
michael@0 | 438 | |
michael@0 | 439 | var cancel = false; |
michael@0 | 440 | |
michael@0 | 441 | // Catch any keys that could potentially move the caret. Ctrl can be |
michael@0 | 442 | // used in combination with these keys on Windows and Linux; and Alt |
michael@0 | 443 | // can be used on OS X, so make sure the unused one isn't used. |
michael@0 | 444 | if (!this.disableKeyNavigation && |
michael@0 | 445 | #ifdef XP_MACOSX |
michael@0 | 446 | !aEvent.ctrlKey) { |
michael@0 | 447 | #else |
michael@0 | 448 | !aEvent.altKey) { |
michael@0 | 449 | #endif |
michael@0 | 450 | switch (aEvent.keyCode) { |
michael@0 | 451 | case KeyEvent.DOM_VK_LEFT: |
michael@0 | 452 | case KeyEvent.DOM_VK_RIGHT: |
michael@0 | 453 | case KeyEvent.DOM_VK_HOME: |
michael@0 | 454 | cancel = this.mController.handleKeyNavigation(aEvent.keyCode); |
michael@0 | 455 | break; |
michael@0 | 456 | } |
michael@0 | 457 | } |
michael@0 | 458 | |
michael@0 | 459 | // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt) |
michael@0 | 460 | if (!this.disableKeyNavigation && !aEvent.ctrlKey && !aEvent.altKey) { |
michael@0 | 461 | switch (aEvent.keyCode) { |
michael@0 | 462 | case KeyEvent.DOM_VK_TAB: |
michael@0 | 463 | if (this.tabScrolling && this.popup.popupOpen) |
michael@0 | 464 | cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ? |
michael@0 | 465 | KeyEvent.DOM_VK_UP : |
michael@0 | 466 | KeyEvent.DOM_VK_DOWN); |
michael@0 | 467 | else if (this.forceComplete && this.mController.matchCount >= 1) |
michael@0 | 468 | this.mController.handleTab(); |
michael@0 | 469 | break; |
michael@0 | 470 | case KeyEvent.DOM_VK_UP: |
michael@0 | 471 | case KeyEvent.DOM_VK_DOWN: |
michael@0 | 472 | case KeyEvent.DOM_VK_PAGE_UP: |
michael@0 | 473 | case KeyEvent.DOM_VK_PAGE_DOWN: |
michael@0 | 474 | cancel = this.mController.handleKeyNavigation(aEvent.keyCode); |
michael@0 | 475 | break; |
michael@0 | 476 | } |
michael@0 | 477 | } |
michael@0 | 478 | |
michael@0 | 479 | // Handle keys we know aren't part of a shortcut, even with Alt or |
michael@0 | 480 | // Ctrl. |
michael@0 | 481 | switch (aEvent.keyCode) { |
michael@0 | 482 | case KeyEvent.DOM_VK_ESCAPE: |
michael@0 | 483 | cancel = this.mController.handleEscape(); |
michael@0 | 484 | break; |
michael@0 | 485 | case KeyEvent.DOM_VK_RETURN: |
michael@0 | 486 | #ifdef XP_MACOSX |
michael@0 | 487 | // Prevent the default action, since it will beep on Mac |
michael@0 | 488 | if (aEvent.metaKey) |
michael@0 | 489 | aEvent.preventDefault(); |
michael@0 | 490 | #endif |
michael@0 | 491 | this.mEnterEvent = aEvent; |
michael@0 | 492 | cancel = this.mController.handleEnter(false); |
michael@0 | 493 | break; |
michael@0 | 494 | case KeyEvent.DOM_VK_DELETE: |
michael@0 | 495 | #ifdef XP_MACOSX |
michael@0 | 496 | case KeyEvent.DOM_VK_BACK_SPACE: |
michael@0 | 497 | if (aEvent.shiftKey) |
michael@0 | 498 | #endif |
michael@0 | 499 | cancel = this.mController.handleDelete(); |
michael@0 | 500 | break; |
michael@0 | 501 | case KeyEvent.DOM_VK_DOWN: |
michael@0 | 502 | case KeyEvent.DOM_VK_UP: |
michael@0 | 503 | if (aEvent.altKey) |
michael@0 | 504 | this.toggleHistoryPopup(); |
michael@0 | 505 | break; |
michael@0 | 506 | #ifndef XP_MACOSX |
michael@0 | 507 | case KeyEvent.DOM_VK_F4: |
michael@0 | 508 | this.toggleHistoryPopup(); |
michael@0 | 509 | break; |
michael@0 | 510 | #endif |
michael@0 | 511 | } |
michael@0 | 512 | |
michael@0 | 513 | if (cancel) { |
michael@0 | 514 | aEvent.stopPropagation(); |
michael@0 | 515 | aEvent.preventDefault(); |
michael@0 | 516 | } |
michael@0 | 517 | |
michael@0 | 518 | return true; |
michael@0 | 519 | ]]></body> |
michael@0 | 520 | </method> |
michael@0 | 521 | |
michael@0 | 522 | <!-- ::::::::::::: miscellaneous ::::::::::::: --> |
michael@0 | 523 | |
michael@0 | 524 | <method name="initSearchNames"> |
michael@0 | 525 | <body><![CDATA[ |
michael@0 | 526 | if (!this.mSearchNames) { |
michael@0 | 527 | var names = this.getAttribute("autocompletesearch"); |
michael@0 | 528 | if (!names) |
michael@0 | 529 | this.mSearchNames = []; |
michael@0 | 530 | else |
michael@0 | 531 | this.mSearchNames = names.split(" "); |
michael@0 | 532 | } |
michael@0 | 533 | ]]></body> |
michael@0 | 534 | </method> |
michael@0 | 535 | |
michael@0 | 536 | <method name="_focus"> |
michael@0 | 537 | <!-- doesn't reset this.mController --> |
michael@0 | 538 | <body><![CDATA[ |
michael@0 | 539 | this._dontBlur = true; |
michael@0 | 540 | this.focus(); |
michael@0 | 541 | this._dontBlur = false; |
michael@0 | 542 | ]]></body> |
michael@0 | 543 | </method> |
michael@0 | 544 | |
michael@0 | 545 | <method name="resetActionType"> |
michael@0 | 546 | <body><![CDATA[ |
michael@0 | 547 | if (this.mIgnoreInput) |
michael@0 | 548 | return; |
michael@0 | 549 | this.removeAttribute("actiontype"); |
michael@0 | 550 | ]]></body> |
michael@0 | 551 | </method> |
michael@0 | 552 | |
michael@0 | 553 | <field name="_valueIsPasted">false</field> |
michael@0 | 554 | <field name="_pasteController"><![CDATA[ |
michael@0 | 555 | ({ |
michael@0 | 556 | _autocomplete: this, |
michael@0 | 557 | _kGlobalClipboard: Components.interfaces.nsIClipboard.kGlobalClipboard, |
michael@0 | 558 | supportsCommand: function(aCommand) aCommand == "cmd_paste", |
michael@0 | 559 | doCommand: function(aCommand) { |
michael@0 | 560 | this._autocomplete._valueIsPasted = true; |
michael@0 | 561 | this._autocomplete.editor.paste(this._kGlobalClipboard); |
michael@0 | 562 | this._autocomplete._valueIsPasted = false; |
michael@0 | 563 | }, |
michael@0 | 564 | isCommandEnabled: function(aCommand) { |
michael@0 | 565 | return this._autocomplete.editor.isSelectionEditable && |
michael@0 | 566 | this._autocomplete.editor.canPaste(this._kGlobalClipboard); |
michael@0 | 567 | }, |
michael@0 | 568 | onEvent: function() {} |
michael@0 | 569 | }) |
michael@0 | 570 | ]]></field> |
michael@0 | 571 | </implementation> |
michael@0 | 572 | |
michael@0 | 573 | <handlers> |
michael@0 | 574 | <handler event="input"><![CDATA[ |
michael@0 | 575 | if (!this.mIgnoreInput && this.mController.input == this) { |
michael@0 | 576 | this.valueIsTyped = true; |
michael@0 | 577 | this.mController.handleText(); |
michael@0 | 578 | } |
michael@0 | 579 | this.resetActionType(); |
michael@0 | 580 | ]]></handler> |
michael@0 | 581 | |
michael@0 | 582 | <handler event="keypress" phase="capturing" |
michael@0 | 583 | action="return this.onKeyPress(event);"/> |
michael@0 | 584 | |
michael@0 | 585 | <handler event="compositionstart" phase="capturing" |
michael@0 | 586 | action="if (this.mController.input == this) this.mController.handleStartComposition();"/> |
michael@0 | 587 | |
michael@0 | 588 | <handler event="compositionend" phase="capturing" |
michael@0 | 589 | action="if (this.mController.input == this) this.mController.handleEndComposition();"/> |
michael@0 | 590 | |
michael@0 | 591 | <handler event="focus" phase="capturing" |
michael@0 | 592 | action="this.attachController();"/> |
michael@0 | 593 | |
michael@0 | 594 | <handler event="blur" phase="capturing" |
michael@0 | 595 | action="if (!this._dontBlur) this.detachController();"/> |
michael@0 | 596 | </handlers> |
michael@0 | 597 | </binding> |
michael@0 | 598 | |
michael@0 | 599 | <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup"> |
michael@0 | 600 | <resources> |
michael@0 | 601 | <stylesheet src="chrome://global/skin/tree.css"/> |
michael@0 | 602 | <stylesheet src="chrome://global/skin/autocomplete.css"/> |
michael@0 | 603 | </resources> |
michael@0 | 604 | |
michael@0 | 605 | <content ignorekeys="true" level="top" consumeoutsideclicks="false"> |
michael@0 | 606 | <xul:tree anonid="tree" class="autocomplete-tree plain" hidecolumnpicker="true" flex="1" seltype="single"> |
michael@0 | 607 | <xul:treecols anonid="treecols"> |
michael@0 | 608 | <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/> |
michael@0 | 609 | </xul:treecols> |
michael@0 | 610 | <xul:treechildren class="autocomplete-treebody"/> |
michael@0 | 611 | </xul:tree> |
michael@0 | 612 | </content> |
michael@0 | 613 | |
michael@0 | 614 | <implementation> |
michael@0 | 615 | <field name="mShowCommentColumn">false</field> |
michael@0 | 616 | <field name="mShowImageColumn">false</field> |
michael@0 | 617 | |
michael@0 | 618 | <property name="showCommentColumn" |
michael@0 | 619 | onget="return this.mShowCommentColumn;"> |
michael@0 | 620 | <setter> |
michael@0 | 621 | <![CDATA[ |
michael@0 | 622 | if (!val && this.mShowCommentColumn) { |
michael@0 | 623 | // reset the flex on the value column and remove the comment column |
michael@0 | 624 | document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 1); |
michael@0 | 625 | this.removeColumn("treecolAutoCompleteComment"); |
michael@0 | 626 | } else if (val && !this.mShowCommentColumn) { |
michael@0 | 627 | // reset the flex on the value column and add the comment column |
michael@0 | 628 | document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 2); |
michael@0 | 629 | this.addColumn({id: "treecolAutoCompleteComment", flex: 1}); |
michael@0 | 630 | } |
michael@0 | 631 | this.mShowCommentColumn = val; |
michael@0 | 632 | return val; |
michael@0 | 633 | ]]> |
michael@0 | 634 | </setter> |
michael@0 | 635 | </property> |
michael@0 | 636 | |
michael@0 | 637 | <property name="showImageColumn" |
michael@0 | 638 | onget="return this.mShowImageColumn;"> |
michael@0 | 639 | <setter> |
michael@0 | 640 | <![CDATA[ |
michael@0 | 641 | if (!val && this.mShowImageColumn) { |
michael@0 | 642 | // remove the image column |
michael@0 | 643 | this.removeColumn("treecolAutoCompleteImage"); |
michael@0 | 644 | } else if (val && !this.mShowImageColumn) { |
michael@0 | 645 | // add the image column |
michael@0 | 646 | this.addColumn({id: "treecolAutoCompleteImage", flex: 1}); |
michael@0 | 647 | } |
michael@0 | 648 | this.mShowImageColumn = val; |
michael@0 | 649 | return val; |
michael@0 | 650 | ]]> |
michael@0 | 651 | </setter> |
michael@0 | 652 | </property> |
michael@0 | 653 | |
michael@0 | 654 | |
michael@0 | 655 | <method name="addColumn"> |
michael@0 | 656 | <parameter name="aAttrs"/> |
michael@0 | 657 | <body> |
michael@0 | 658 | <![CDATA[ |
michael@0 | 659 | var col = document.createElement("treecol"); |
michael@0 | 660 | col.setAttribute("class", "autocomplete-treecol"); |
michael@0 | 661 | for (var name in aAttrs) |
michael@0 | 662 | col.setAttribute(name, aAttrs[name]); |
michael@0 | 663 | this.treecols.appendChild(col); |
michael@0 | 664 | return col; |
michael@0 | 665 | ]]> |
michael@0 | 666 | </body> |
michael@0 | 667 | </method> |
michael@0 | 668 | |
michael@0 | 669 | <method name="removeColumn"> |
michael@0 | 670 | <parameter name="aColId"/> |
michael@0 | 671 | <body> |
michael@0 | 672 | <![CDATA[ |
michael@0 | 673 | return this.treecols.removeChild(document.getElementById(aColId)); |
michael@0 | 674 | ]]> |
michael@0 | 675 | </body> |
michael@0 | 676 | </method> |
michael@0 | 677 | |
michael@0 | 678 | <property name="selectedIndex" |
michael@0 | 679 | onget="return this.tree.currentIndex;"> |
michael@0 | 680 | <setter> |
michael@0 | 681 | <![CDATA[ |
michael@0 | 682 | this.tree.view.selection.select(val); |
michael@0 | 683 | if (this.tree.treeBoxObject.height > 0) |
michael@0 | 684 | this.tree.treeBoxObject.ensureRowIsVisible(val < 0 ? 0 : val); |
michael@0 | 685 | // Fire select event on xul:tree so that accessibility API |
michael@0 | 686 | // support layer can fire appropriate accessibility events. |
michael@0 | 687 | var event = document.createEvent('Events'); |
michael@0 | 688 | event.initEvent("select", true, true); |
michael@0 | 689 | this.tree.dispatchEvent(event); |
michael@0 | 690 | return val; |
michael@0 | 691 | ]]></setter> |
michael@0 | 692 | </property> |
michael@0 | 693 | |
michael@0 | 694 | <method name="adjustHeight"> |
michael@0 | 695 | <body> |
michael@0 | 696 | <![CDATA[ |
michael@0 | 697 | // detect the desired height of the tree |
michael@0 | 698 | var bx = this.tree.treeBoxObject; |
michael@0 | 699 | var view = this.tree.view; |
michael@0 | 700 | if (!view) |
michael@0 | 701 | return; |
michael@0 | 702 | var rows = this.maxRows; |
michael@0 | 703 | if (!view.rowCount || (rows && view.rowCount < rows)) |
michael@0 | 704 | rows = view.rowCount; |
michael@0 | 705 | |
michael@0 | 706 | var height = rows * bx.rowHeight; |
michael@0 | 707 | |
michael@0 | 708 | if (height == 0) |
michael@0 | 709 | this.tree.setAttribute("collapsed", "true"); |
michael@0 | 710 | else { |
michael@0 | 711 | if (this.tree.hasAttribute("collapsed")) |
michael@0 | 712 | this.tree.removeAttribute("collapsed"); |
michael@0 | 713 | |
michael@0 | 714 | this.tree.setAttribute("height", height); |
michael@0 | 715 | } |
michael@0 | 716 | this.tree.setAttribute("hidescrollbar", view.rowCount <= rows); |
michael@0 | 717 | ]]> |
michael@0 | 718 | </body> |
michael@0 | 719 | </method> |
michael@0 | 720 | |
michael@0 | 721 | <method name="openAutocompletePopup"> |
michael@0 | 722 | <parameter name="aInput"/> |
michael@0 | 723 | <parameter name="aElement"/> |
michael@0 | 724 | <body><![CDATA[ |
michael@0 | 725 | // until we have "baseBinding", (see bug #373652) this allows |
michael@0 | 726 | // us to override openAutocompletePopup(), but still call |
michael@0 | 727 | // the method on the base class |
michael@0 | 728 | this._openAutocompletePopup(aInput, aElement); |
michael@0 | 729 | ]]></body> |
michael@0 | 730 | </method> |
michael@0 | 731 | |
michael@0 | 732 | <method name="_openAutocompletePopup"> |
michael@0 | 733 | <parameter name="aInput"/> |
michael@0 | 734 | <parameter name="aElement"/> |
michael@0 | 735 | <body><![CDATA[ |
michael@0 | 736 | if (!this.mPopupOpen) { |
michael@0 | 737 | this.mInput = aInput; |
michael@0 | 738 | this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView); |
michael@0 | 739 | this.invalidate(); |
michael@0 | 740 | |
michael@0 | 741 | this.showCommentColumn = this.mInput.showCommentColumn; |
michael@0 | 742 | this.showImageColumn = this.mInput.showImageColumn; |
michael@0 | 743 | |
michael@0 | 744 | var rect = aElement.getBoundingClientRect(); |
michael@0 | 745 | var nav = aElement.ownerDocument.defaultView.QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
michael@0 | 746 | .getInterface(Components.interfaces.nsIWebNavigation); |
michael@0 | 747 | var docShell = nav.QueryInterface(Components.interfaces.nsIDocShell); |
michael@0 | 748 | var docViewer = docShell.contentViewer.QueryInterface(Components.interfaces.nsIMarkupDocumentViewer); |
michael@0 | 749 | var width = (rect.right - rect.left) * docViewer.fullZoom; |
michael@0 | 750 | this.setAttribute("width", width > 100 ? width : 100); |
michael@0 | 751 | |
michael@0 | 752 | // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840 |
michael@0 | 753 | var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction; |
michael@0 | 754 | this.style.direction = popupDirection; |
michael@0 | 755 | |
michael@0 | 756 | this.openPopup(aElement, "after_start", 0, 0, false, false); |
michael@0 | 757 | } |
michael@0 | 758 | ]]></body> |
michael@0 | 759 | </method> |
michael@0 | 760 | |
michael@0 | 761 | <method name="invalidate"> |
michael@0 | 762 | <body><![CDATA[ |
michael@0 | 763 | this.adjustHeight(); |
michael@0 | 764 | this.tree.treeBoxObject.invalidate(); |
michael@0 | 765 | ]]></body> |
michael@0 | 766 | </method> |
michael@0 | 767 | |
michael@0 | 768 | <method name="selectBy"> |
michael@0 | 769 | <parameter name="aReverse"/> |
michael@0 | 770 | <parameter name="aPage"/> |
michael@0 | 771 | <body><![CDATA[ |
michael@0 | 772 | try { |
michael@0 | 773 | var amount = aPage ? 5 : 1; |
michael@0 | 774 | this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.tree.view.rowCount-1); |
michael@0 | 775 | if (this.selectedIndex == -1) { |
michael@0 | 776 | this.input._focus(); |
michael@0 | 777 | } |
michael@0 | 778 | } catch (ex) { |
michael@0 | 779 | // do nothing - occasionally timer-related js errors happen here |
michael@0 | 780 | // e.g. "this.selectedIndex has no properties", when you type fast and hit a |
michael@0 | 781 | // navigation key before this popup has opened |
michael@0 | 782 | } |
michael@0 | 783 | ]]></body> |
michael@0 | 784 | </method> |
michael@0 | 785 | |
michael@0 | 786 | <!-- =================== PUBLIC MEMBERS =================== --> |
michael@0 | 787 | |
michael@0 | 788 | <field name="tree"> |
michael@0 | 789 | document.getAnonymousElementByAttribute(this, "anonid", "tree"); |
michael@0 | 790 | </field> |
michael@0 | 791 | |
michael@0 | 792 | <field name="treecols"> |
michael@0 | 793 | document.getAnonymousElementByAttribute(this, "anonid", "treecols"); |
michael@0 | 794 | </field> |
michael@0 | 795 | |
michael@0 | 796 | <property name="view" |
michael@0 | 797 | onget="return this.mView;"> |
michael@0 | 798 | <setter><![CDATA[ |
michael@0 | 799 | // We must do this by hand because the tree binding may not be ready yet |
michael@0 | 800 | this.mView = val; |
michael@0 | 801 | var bx = this.tree.boxObject; |
michael@0 | 802 | bx = bx.QueryInterface(Components.interfaces.nsITreeBoxObject); |
michael@0 | 803 | bx.view = val; |
michael@0 | 804 | ]]></setter> |
michael@0 | 805 | </property> |
michael@0 | 806 | |
michael@0 | 807 | </implementation> |
michael@0 | 808 | </binding> |
michael@0 | 809 | |
michael@0 | 810 | <binding id="autocomplete-base-popup" role="none" |
michael@0 | 811 | extends="chrome://global/content/bindings/popup.xml#popup"> |
michael@0 | 812 | <implementation implements="nsIAutoCompletePopup"> |
michael@0 | 813 | <field name="mInput">null</field> |
michael@0 | 814 | <field name="mPopupOpen">false</field> |
michael@0 | 815 | |
michael@0 | 816 | <!-- =================== nsIAutoCompletePopup =================== --> |
michael@0 | 817 | |
michael@0 | 818 | <property name="input" readonly="true" |
michael@0 | 819 | onget="return this.mInput"/> |
michael@0 | 820 | |
michael@0 | 821 | <property name="overrideValue" readonly="true" |
michael@0 | 822 | onget="return null;"/> |
michael@0 | 823 | |
michael@0 | 824 | <property name="popupOpen" readonly="true" |
michael@0 | 825 | onget="return this.mPopupOpen;"/> |
michael@0 | 826 | |
michael@0 | 827 | <method name="closePopup"> |
michael@0 | 828 | <body> |
michael@0 | 829 | <![CDATA[ |
michael@0 | 830 | if (this.mPopupOpen) { |
michael@0 | 831 | this.hidePopup(); |
michael@0 | 832 | this.removeAttribute("width"); |
michael@0 | 833 | } |
michael@0 | 834 | ]]> |
michael@0 | 835 | </body> |
michael@0 | 836 | </method> |
michael@0 | 837 | |
michael@0 | 838 | <!-- This is the default number of rows that we give the autocomplete |
michael@0 | 839 | popup when the textbox doesn't have a "maxrows" attribute |
michael@0 | 840 | for us to use. --> |
michael@0 | 841 | <field name="defaultMaxRows" readonly="true">6</field> |
michael@0 | 842 | |
michael@0 | 843 | <!-- In some cases (e.g. when the input's dropmarker button is clicked), |
michael@0 | 844 | the input wants to display a popup with more rows. In that case, it |
michael@0 | 845 | should increase its maxRows property and store the "normal" maxRows |
michael@0 | 846 | in this field. When the popup is hidden, we restore the input's |
michael@0 | 847 | maxRows to the value stored in this field. |
michael@0 | 848 | |
michael@0 | 849 | This field is set to -1 between uses so that we can tell when it's |
michael@0 | 850 | been set by the input and when we need to set it in the popupshowing |
michael@0 | 851 | handler. --> |
michael@0 | 852 | <field name="_normalMaxRows">-1</field> |
michael@0 | 853 | |
michael@0 | 854 | <property name="maxRows" readonly="true"> |
michael@0 | 855 | <getter> |
michael@0 | 856 | <![CDATA[ |
michael@0 | 857 | return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows; |
michael@0 | 858 | ]]> |
michael@0 | 859 | </getter> |
michael@0 | 860 | </property> |
michael@0 | 861 | |
michael@0 | 862 | <method name="getNextIndex"> |
michael@0 | 863 | <parameter name="aReverse"/> |
michael@0 | 864 | <parameter name="aAmount"/> |
michael@0 | 865 | <parameter name="aIndex"/> |
michael@0 | 866 | <parameter name="aMaxRow"/> |
michael@0 | 867 | <body><![CDATA[ |
michael@0 | 868 | if (aMaxRow < 0) |
michael@0 | 869 | return -1; |
michael@0 | 870 | |
michael@0 | 871 | var newIdx = aIndex + (aReverse?-1:1)*aAmount; |
michael@0 | 872 | if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow) |
michael@0 | 873 | newIdx = aMaxRow; |
michael@0 | 874 | else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0) |
michael@0 | 875 | newIdx = 0; |
michael@0 | 876 | |
michael@0 | 877 | if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow) |
michael@0 | 878 | aIndex = -1; |
michael@0 | 879 | else |
michael@0 | 880 | aIndex = newIdx; |
michael@0 | 881 | |
michael@0 | 882 | return aIndex; |
michael@0 | 883 | ]]></body> |
michael@0 | 884 | </method> |
michael@0 | 885 | |
michael@0 | 886 | <method name="onPopupClick"> |
michael@0 | 887 | <parameter name="aEvent"/> |
michael@0 | 888 | <body><![CDATA[ |
michael@0 | 889 | var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); |
michael@0 | 890 | controller.handleEnter(true); |
michael@0 | 891 | ]]></body> |
michael@0 | 892 | </method> |
michael@0 | 893 | </implementation> |
michael@0 | 894 | |
michael@0 | 895 | <handlers> |
michael@0 | 896 | <handler event="popupshowing"><![CDATA[ |
michael@0 | 897 | // If normalMaxRows wasn't already set by the input, then set it here |
michael@0 | 898 | // so that we restore the correct number when the popup is hidden. |
michael@0 | 899 | if (this._normalMaxRows < 0) |
michael@0 | 900 | this._normalMaxRows = this.mInput.maxRows; |
michael@0 | 901 | |
michael@0 | 902 | this.mPopupOpen = true; |
michael@0 | 903 | ]]></handler> |
michael@0 | 904 | |
michael@0 | 905 | <handler event="popuphiding"><![CDATA[ |
michael@0 | 906 | var isListActive = true; |
michael@0 | 907 | if (this.selectedIndex == -1) |
michael@0 | 908 | isListActive = false; |
michael@0 | 909 | var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); |
michael@0 | 910 | controller.stopSearch(); |
michael@0 | 911 | |
michael@0 | 912 | this.mPopupOpen = false; |
michael@0 | 913 | |
michael@0 | 914 | // Reset the maxRows property to the cached "normal" value, and reset |
michael@0 | 915 | // _normalMaxRows so that we can detect whether it was set by the input |
michael@0 | 916 | // when the popupshowing handler runs. |
michael@0 | 917 | this.mInput.maxRows = this._normalMaxRows; |
michael@0 | 918 | this._normalMaxRows = -1; |
michael@0 | 919 | // If the list was being navigated and then closed, make sure |
michael@0 | 920 | // we fire accessible focus event back to textbox |
michael@0 | 921 | if (isListActive) { |
michael@0 | 922 | this.mInput.mIgnoreFocus = true; |
michael@0 | 923 | this.mInput._focus(); |
michael@0 | 924 | this.mInput.mIgnoreFocus = false; |
michael@0 | 925 | } |
michael@0 | 926 | ]]></handler> |
michael@0 | 927 | </handlers> |
michael@0 | 928 | </binding> |
michael@0 | 929 | |
michael@0 | 930 | <binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup"> |
michael@0 | 931 | <resources> |
michael@0 | 932 | <stylesheet src="chrome://global/skin/autocomplete.css"/> |
michael@0 | 933 | </resources> |
michael@0 | 934 | |
michael@0 | 935 | <content ignorekeys="true" level="top" consumeoutsideclicks="false"> |
michael@0 | 936 | <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/> |
michael@0 | 937 | <xul:hbox> |
michael@0 | 938 | <children/> |
michael@0 | 939 | </xul:hbox> |
michael@0 | 940 | </content> |
michael@0 | 941 | |
michael@0 | 942 | <implementation implements="nsIAutoCompletePopup"> |
michael@0 | 943 | <field name="_currentIndex">0</field> |
michael@0 | 944 | <field name="_rowHeight">0</field> |
michael@0 | 945 | |
michael@0 | 946 | <!-- =================== nsIAutoCompletePopup =================== --> |
michael@0 | 947 | |
michael@0 | 948 | <property name="selectedIndex" |
michael@0 | 949 | onget="return this.richlistbox.selectedIndex;"> |
michael@0 | 950 | <setter> |
michael@0 | 951 | <![CDATA[ |
michael@0 | 952 | this.richlistbox.selectedIndex = val; |
michael@0 | 953 | |
michael@0 | 954 | // when clearing the selection (val == -1, so selectedItem will be |
michael@0 | 955 | // null), we want to scroll back to the top. see bug #406194 |
michael@0 | 956 | this.richlistbox.ensureElementIsVisible( |
michael@0 | 957 | this.richlistbox.selectedItem || this.richlistbox.firstChild); |
michael@0 | 958 | |
michael@0 | 959 | return val; |
michael@0 | 960 | ]]> |
michael@0 | 961 | </setter> |
michael@0 | 962 | </property> |
michael@0 | 963 | |
michael@0 | 964 | <method name="openAutocompletePopup"> |
michael@0 | 965 | <parameter name="aInput"/> |
michael@0 | 966 | <parameter name="aElement"/> |
michael@0 | 967 | <body> |
michael@0 | 968 | <![CDATA[ |
michael@0 | 969 | // until we have "baseBinding", (see bug #373652) this allows |
michael@0 | 970 | // us to override openAutocompletePopup(), but still call |
michael@0 | 971 | // the method on the base class |
michael@0 | 972 | this._openAutocompletePopup(aInput, aElement); |
michael@0 | 973 | ]]> |
michael@0 | 974 | </body> |
michael@0 | 975 | </method> |
michael@0 | 976 | |
michael@0 | 977 | <method name="_openAutocompletePopup"> |
michael@0 | 978 | <parameter name="aInput"/> |
michael@0 | 979 | <parameter name="aElement"/> |
michael@0 | 980 | <body> |
michael@0 | 981 | <![CDATA[ |
michael@0 | 982 | if (!this.mPopupOpen) { |
michael@0 | 983 | this.mInput = aInput; |
michael@0 | 984 | // clear any previous selection, see bugs 400671 and 488357 |
michael@0 | 985 | this.selectedIndex = -1; |
michael@0 | 986 | |
michael@0 | 987 | var width = aElement.getBoundingClientRect().width; |
michael@0 | 988 | this.setAttribute("width", width > 100 ? width : 100); |
michael@0 | 989 | // invalidate() depends on the width attribute |
michael@0 | 990 | this._invalidate(); |
michael@0 | 991 | |
michael@0 | 992 | this.openPopup(aElement, "after_start", 0, 0, false, false); |
michael@0 | 993 | } |
michael@0 | 994 | ]]> |
michael@0 | 995 | </body> |
michael@0 | 996 | </method> |
michael@0 | 997 | |
michael@0 | 998 | <method name="invalidate"> |
michael@0 | 999 | <body> |
michael@0 | 1000 | <![CDATA[ |
michael@0 | 1001 | // Don't bother doing work if we're not even showing |
michael@0 | 1002 | if (!this.mPopupOpen) |
michael@0 | 1003 | return; |
michael@0 | 1004 | |
michael@0 | 1005 | this._invalidate(); |
michael@0 | 1006 | ]]> |
michael@0 | 1007 | </body> |
michael@0 | 1008 | </method> |
michael@0 | 1009 | |
michael@0 | 1010 | <method name="_invalidate"> |
michael@0 | 1011 | <body> |
michael@0 | 1012 | <![CDATA[ |
michael@0 | 1013 | if (!this.hasAttribute("height")) { |
michael@0 | 1014 | // collapsed if no matches |
michael@0 | 1015 | this.richlistbox.collapsed = (this._matchCount == 0); |
michael@0 | 1016 | |
michael@0 | 1017 | // Dynamically update height until richlistbox.rows works (bug 401939) |
michael@0 | 1018 | // Adjust the height immediately and after the row contents update |
michael@0 | 1019 | this.adjustHeight(); |
michael@0 | 1020 | setTimeout(function(self) self.adjustHeight(), 0, this); |
michael@0 | 1021 | } |
michael@0 | 1022 | |
michael@0 | 1023 | // make sure to collapse any existing richlistitems |
michael@0 | 1024 | // that aren't going to be used |
michael@0 | 1025 | var existingItemsCount = this.richlistbox.childNodes.length; |
michael@0 | 1026 | for (var i = this._matchCount; i < existingItemsCount; i++) |
michael@0 | 1027 | this.richlistbox.childNodes[i].collapsed = true; |
michael@0 | 1028 | |
michael@0 | 1029 | this._currentIndex = 0; |
michael@0 | 1030 | this._appendCurrentResult(); |
michael@0 | 1031 | ]]> |
michael@0 | 1032 | </body> |
michael@0 | 1033 | </method> |
michael@0 | 1034 | |
michael@0 | 1035 | <property name="maxResults" readonly="true"> |
michael@0 | 1036 | <getter> |
michael@0 | 1037 | <![CDATA[ |
michael@0 | 1038 | // this is how many richlistitems will be kept around |
michael@0 | 1039 | // (note, this getter may be overridden) |
michael@0 | 1040 | return 20; |
michael@0 | 1041 | ]]> |
michael@0 | 1042 | </getter> |
michael@0 | 1043 | </property> |
michael@0 | 1044 | |
michael@0 | 1045 | <property name="_matchCount" readonly="true"> |
michael@0 | 1046 | <getter> |
michael@0 | 1047 | <![CDATA[ |
michael@0 | 1048 | return Math.min(this.mInput.controller.matchCount, this.maxResults); |
michael@0 | 1049 | ]]> |
michael@0 | 1050 | </getter> |
michael@0 | 1051 | </property> |
michael@0 | 1052 | |
michael@0 | 1053 | <method name="adjustHeight"> |
michael@0 | 1054 | <body> |
michael@0 | 1055 | <![CDATA[ |
michael@0 | 1056 | // Figure out how many rows to show |
michael@0 | 1057 | let rows = this.richlistbox.childNodes; |
michael@0 | 1058 | let numRows = Math.min(this._matchCount, this.maxRows, rows.length); |
michael@0 | 1059 | |
michael@0 | 1060 | // Default the height to 0 if we have no rows to show |
michael@0 | 1061 | let height = 0; |
michael@0 | 1062 | if (numRows) { |
michael@0 | 1063 | if (!this._rowHeight) { |
michael@0 | 1064 | let firstRowRect = rows[0].getBoundingClientRect(); |
michael@0 | 1065 | this._rowHeight = firstRowRect.height; |
michael@0 | 1066 | } |
michael@0 | 1067 | |
michael@0 | 1068 | // Calculate the height to have the first row to last row shown |
michael@0 | 1069 | height = this._rowHeight * numRows; |
michael@0 | 1070 | } |
michael@0 | 1071 | |
michael@0 | 1072 | // Only update the height if we have a non-zero height and if it |
michael@0 | 1073 | // changed (the richlistbox is collapsed if there are no results) |
michael@0 | 1074 | if (height && height != this.richlistbox.height) |
michael@0 | 1075 | this.richlistbox.height = height; |
michael@0 | 1076 | ]]> |
michael@0 | 1077 | </body> |
michael@0 | 1078 | </method> |
michael@0 | 1079 | |
michael@0 | 1080 | <method name="_appendCurrentResult"> |
michael@0 | 1081 | <body> |
michael@0 | 1082 | <![CDATA[ |
michael@0 | 1083 | var controller = this.mInput.controller; |
michael@0 | 1084 | var matchCount = this._matchCount; |
michael@0 | 1085 | var existingItemsCount = this.richlistbox.childNodes.length; |
michael@0 | 1086 | |
michael@0 | 1087 | // Process maxRows per chunk to improve performance and user experience |
michael@0 | 1088 | for (let i = 0; i < this.maxRows; i++) { |
michael@0 | 1089 | if (this._currentIndex >= matchCount) |
michael@0 | 1090 | return; |
michael@0 | 1091 | |
michael@0 | 1092 | var item; |
michael@0 | 1093 | |
michael@0 | 1094 | // trim the leading/trailing whitespace |
michael@0 | 1095 | var trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, ""); |
michael@0 | 1096 | |
michael@0 | 1097 | // Unescape the URI spec for showing as an entry in the popup |
michael@0 | 1098 | let url = Components.classes["@mozilla.org/intl/texttosuburi;1"]. |
michael@0 | 1099 | getService(Components.interfaces.nsITextToSubURI). |
michael@0 | 1100 | unEscapeURIForUI("UTF-8", controller.getValueAt(this._currentIndex)); |
michael@0 | 1101 | |
michael@0 | 1102 | if (typeof this.input.trimValue == "function") |
michael@0 | 1103 | url = this.input.trimValue(url); |
michael@0 | 1104 | |
michael@0 | 1105 | if (this._currentIndex < existingItemsCount) { |
michael@0 | 1106 | // re-use the existing item |
michael@0 | 1107 | item = this.richlistbox.childNodes[this._currentIndex]; |
michael@0 | 1108 | |
michael@0 | 1109 | // Completely re-use the existing richlistitem if it's the same |
michael@0 | 1110 | if (item.getAttribute("text") == trimmedSearchString && |
michael@0 | 1111 | item.getAttribute("url") == url) { |
michael@0 | 1112 | item.collapsed = false; |
michael@0 | 1113 | this._currentIndex++; |
michael@0 | 1114 | continue; |
michael@0 | 1115 | } |
michael@0 | 1116 | } |
michael@0 | 1117 | else { |
michael@0 | 1118 | // need to create a new item |
michael@0 | 1119 | item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem"); |
michael@0 | 1120 | } |
michael@0 | 1121 | |
michael@0 | 1122 | // set these attributes before we set the class |
michael@0 | 1123 | // so that we can use them from the constructor |
michael@0 | 1124 | item.setAttribute("image", controller.getImageAt(this._currentIndex)); |
michael@0 | 1125 | item.setAttribute("url", url); |
michael@0 | 1126 | item.setAttribute("title", controller.getCommentAt(this._currentIndex)); |
michael@0 | 1127 | item.setAttribute("type", controller.getStyleAt(this._currentIndex)); |
michael@0 | 1128 | item.setAttribute("text", trimmedSearchString); |
michael@0 | 1129 | |
michael@0 | 1130 | if (this._currentIndex < existingItemsCount) { |
michael@0 | 1131 | // re-use the existing item |
michael@0 | 1132 | item._adjustAcItem(); |
michael@0 | 1133 | item.collapsed = false; |
michael@0 | 1134 | } |
michael@0 | 1135 | else { |
michael@0 | 1136 | // set the class at the end so we can use the attributes |
michael@0 | 1137 | // in the xbl constructor |
michael@0 | 1138 | item.className = "autocomplete-richlistitem"; |
michael@0 | 1139 | this.richlistbox.appendChild(item); |
michael@0 | 1140 | } |
michael@0 | 1141 | |
michael@0 | 1142 | this._currentIndex++; |
michael@0 | 1143 | } |
michael@0 | 1144 | |
michael@0 | 1145 | // yield after each batch of items so that typing the url bar is responsive |
michael@0 | 1146 | setTimeout(function (self) { self._appendCurrentResult(); }, 0, this); |
michael@0 | 1147 | ]]> |
michael@0 | 1148 | </body> |
michael@0 | 1149 | </method> |
michael@0 | 1150 | |
michael@0 | 1151 | <method name="selectBy"> |
michael@0 | 1152 | <parameter name="aReverse"/> |
michael@0 | 1153 | <parameter name="aPage"/> |
michael@0 | 1154 | <body> |
michael@0 | 1155 | <![CDATA[ |
michael@0 | 1156 | try { |
michael@0 | 1157 | var amount = aPage ? 5 : 1; |
michael@0 | 1158 | |
michael@0 | 1159 | // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount |
michael@0 | 1160 | this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this._matchCount - 1); |
michael@0 | 1161 | if (this.selectedIndex == -1) { |
michael@0 | 1162 | this.input._focus(); |
michael@0 | 1163 | } |
michael@0 | 1164 | } catch (ex) { |
michael@0 | 1165 | // do nothing - occasionally timer-related js errors happen here |
michael@0 | 1166 | // e.g. "this.selectedIndex has no properties", when you type fast and hit a |
michael@0 | 1167 | // navigation key before this popup has opened |
michael@0 | 1168 | } |
michael@0 | 1169 | ]]> |
michael@0 | 1170 | </body> |
michael@0 | 1171 | </method> |
michael@0 | 1172 | |
michael@0 | 1173 | <field name="richlistbox"> |
michael@0 | 1174 | document.getAnonymousElementByAttribute(this, "anonid", "richlistbox"); |
michael@0 | 1175 | </field> |
michael@0 | 1176 | |
michael@0 | 1177 | <property name="view" |
michael@0 | 1178 | onget="return this.mInput.controller;" |
michael@0 | 1179 | onset="return val;"/> |
michael@0 | 1180 | |
michael@0 | 1181 | </implementation> |
michael@0 | 1182 | </binding> |
michael@0 | 1183 | |
michael@0 | 1184 | <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> |
michael@0 | 1185 | <content> |
michael@0 | 1186 | <xul:hbox align="center" class="ac-title-box"> |
michael@0 | 1187 | <xul:image xbl:inherits="src=image" class="ac-site-icon"/> |
michael@0 | 1188 | <xul:hbox anonid="title-box" class="ac-title" flex="1" |
michael@0 | 1189 | onunderflow="_doUnderflow('_title');" |
michael@0 | 1190 | onoverflow="_doOverflow('_title');"> |
michael@0 | 1191 | <xul:description anonid="title" class="ac-normal-text ac-comment" xbl:inherits="selected"/> |
michael@0 | 1192 | </xul:hbox> |
michael@0 | 1193 | <xul:label anonid="title-overflow-ellipsis" xbl:inherits="selected" |
michael@0 | 1194 | class="ac-ellipsis-after ac-comment"/> |
michael@0 | 1195 | <xul:hbox anonid="extra-box" class="ac-extra" align="center" hidden="true"> |
michael@0 | 1196 | <xul:image class="ac-result-type-tag"/> |
michael@0 | 1197 | <xul:label class="ac-normal-text ac-comment" xbl:inherits="selected" value=":"/> |
michael@0 | 1198 | <xul:description anonid="extra" class="ac-normal-text ac-comment" xbl:inherits="selected"/> |
michael@0 | 1199 | </xul:hbox> |
michael@0 | 1200 | <xul:image anonid="type-image" class="ac-type-icon"/> |
michael@0 | 1201 | </xul:hbox> |
michael@0 | 1202 | <xul:hbox align="center" class="ac-url-box"> |
michael@0 | 1203 | <xul:spacer class="ac-site-icon"/> |
michael@0 | 1204 | <xul:image class="ac-action-icon"/> |
michael@0 | 1205 | <xul:hbox anonid="url-box" class="ac-url" flex="1" |
michael@0 | 1206 | onunderflow="_doUnderflow('_url');" |
michael@0 | 1207 | onoverflow="_doOverflow('_url');"> |
michael@0 | 1208 | <xul:description anonid="url" class="ac-normal-text ac-url-text" |
michael@0 | 1209 | xbl:inherits="selected type"/> |
michael@0 | 1210 | <xul:description anonid="action" class="ac-normal-text ac-action-text" |
michael@0 | 1211 | xbl:inherits="selected type"/> |
michael@0 | 1212 | </xul:hbox> |
michael@0 | 1213 | <xul:label anonid="url-overflow-ellipsis" xbl:inherits="selected" |
michael@0 | 1214 | class="ac-ellipsis-after ac-url-text"/> |
michael@0 | 1215 | <xul:spacer class="ac-type-icon"/> |
michael@0 | 1216 | </xul:hbox> |
michael@0 | 1217 | </content> |
michael@0 | 1218 | <implementation implements="nsIDOMXULSelectControlItemElement"> |
michael@0 | 1219 | <constructor> |
michael@0 | 1220 | <![CDATA[ |
michael@0 | 1221 | let ellipsis = "\u2026"; |
michael@0 | 1222 | try { |
michael@0 | 1223 | ellipsis = Components.classes["@mozilla.org/preferences-service;1"]. |
michael@0 | 1224 | getService(Components.interfaces.nsIPrefBranch). |
michael@0 | 1225 | getComplexValue("intl.ellipsis", |
michael@0 | 1226 | Components.interfaces.nsIPrefLocalizedString).data; |
michael@0 | 1227 | } catch (ex) { |
michael@0 | 1228 | // Do nothing.. we already have a default |
michael@0 | 1229 | } |
michael@0 | 1230 | |
michael@0 | 1231 | this._urlOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "url-overflow-ellipsis"); |
michael@0 | 1232 | this._titleOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "title-overflow-ellipsis"); |
michael@0 | 1233 | |
michael@0 | 1234 | this._urlOverflowEllipsis.value = ellipsis; |
michael@0 | 1235 | this._titleOverflowEllipsis.value = ellipsis; |
michael@0 | 1236 | |
michael@0 | 1237 | this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image"); |
michael@0 | 1238 | |
michael@0 | 1239 | this._urlBox = document.getAnonymousElementByAttribute(this, "anonid", "url-box"); |
michael@0 | 1240 | this._url = document.getAnonymousElementByAttribute(this, "anonid", "url"); |
michael@0 | 1241 | this._action = document.getAnonymousElementByAttribute(this, "anonid", "action"); |
michael@0 | 1242 | |
michael@0 | 1243 | this._titleBox = document.getAnonymousElementByAttribute(this, "anonid", "title-box"); |
michael@0 | 1244 | this._title = document.getAnonymousElementByAttribute(this, "anonid", "title"); |
michael@0 | 1245 | |
michael@0 | 1246 | this._extraBox = document.getAnonymousElementByAttribute(this, "anonid", "extra-box"); |
michael@0 | 1247 | this._extra = document.getAnonymousElementByAttribute(this, "anonid", "extra"); |
michael@0 | 1248 | |
michael@0 | 1249 | this._adjustAcItem(); |
michael@0 | 1250 | ]]> |
michael@0 | 1251 | </constructor> |
michael@0 | 1252 | |
michael@0 | 1253 | <property name="label" readonly="true"> |
michael@0 | 1254 | <getter> |
michael@0 | 1255 | <![CDATA[ |
michael@0 | 1256 | var title = this.getAttribute("title"); |
michael@0 | 1257 | var url = this.getAttribute("url"); |
michael@0 | 1258 | var panel = this.parentNode.parentNode; |
michael@0 | 1259 | |
michael@0 | 1260 | // allow consumers that have extended popups to override |
michael@0 | 1261 | // the label values for the richlistitems |
michael@0 | 1262 | if (panel.createResultLabel) |
michael@0 | 1263 | return panel.createResultLabel(title, url, this.getAttribute("type")); |
michael@0 | 1264 | |
michael@0 | 1265 | // aType (ex: "ac-result-type-<aType>") is related to the class of the image, |
michael@0 | 1266 | // and is not "visible" text so don't use it for the label (for accessibility). |
michael@0 | 1267 | return title + " " + url; |
michael@0 | 1268 | ]]> |
michael@0 | 1269 | </getter> |
michael@0 | 1270 | </property> |
michael@0 | 1271 | |
michael@0 | 1272 | <field name="_boundaryCutoff">null</field> |
michael@0 | 1273 | |
michael@0 | 1274 | <property name="boundaryCutoff" readonly="true"> |
michael@0 | 1275 | <getter> |
michael@0 | 1276 | <![CDATA[ |
michael@0 | 1277 | if (!this._boundaryCutoff) { |
michael@0 | 1278 | this._boundaryCutoff = |
michael@0 | 1279 | Components.classes["@mozilla.org/preferences-service;1"]. |
michael@0 | 1280 | getService(Components.interfaces.nsIPrefBranch). |
michael@0 | 1281 | getIntPref("toolkit.autocomplete.richBoundaryCutoff"); |
michael@0 | 1282 | } |
michael@0 | 1283 | return this._boundaryCutoff; |
michael@0 | 1284 | ]]> |
michael@0 | 1285 | </getter> |
michael@0 | 1286 | </property> |
michael@0 | 1287 | |
michael@0 | 1288 | <method name="_getBoundaryIndices"> |
michael@0 | 1289 | <parameter name="aText"/> |
michael@0 | 1290 | <parameter name="aSearchTokens"/> |
michael@0 | 1291 | <body> |
michael@0 | 1292 | <![CDATA[ |
michael@0 | 1293 | // Short circuit for empty search ([""] == "") |
michael@0 | 1294 | if (aSearchTokens == "") |
michael@0 | 1295 | return [0, aText.length]; |
michael@0 | 1296 | |
michael@0 | 1297 | // Find which regions of text match the search terms |
michael@0 | 1298 | let regions = []; |
michael@0 | 1299 | for each (let search in aSearchTokens) { |
michael@0 | 1300 | let matchIndex; |
michael@0 | 1301 | let startIndex = 0; |
michael@0 | 1302 | let searchLen = search.length; |
michael@0 | 1303 | |
michael@0 | 1304 | // Find all matches of the search terms, but stop early for perf |
michael@0 | 1305 | let lowerText = aText.toLowerCase().substr(0, this.boundaryCutoff); |
michael@0 | 1306 | while ((matchIndex = lowerText.indexOf(search, startIndex)) >= 0) { |
michael@0 | 1307 | // Start the next search from where this one finished |
michael@0 | 1308 | startIndex = matchIndex + searchLen; |
michael@0 | 1309 | regions.push([matchIndex, startIndex]); |
michael@0 | 1310 | } |
michael@0 | 1311 | } |
michael@0 | 1312 | |
michael@0 | 1313 | // Sort the regions by start position then end position |
michael@0 | 1314 | regions = regions.sort(function(a, b) let (start = a[0] - b[0]) |
michael@0 | 1315 | start == 0 ? a[1] - b[1] : start); |
michael@0 | 1316 | |
michael@0 | 1317 | // Generate the boundary indices from each region |
michael@0 | 1318 | let start = 0; |
michael@0 | 1319 | let end = 0; |
michael@0 | 1320 | let boundaries = []; |
michael@0 | 1321 | let len = regions.length; |
michael@0 | 1322 | for (let i = 0; i < len; i++) { |
michael@0 | 1323 | // We have a new boundary if the start of the next is past the end |
michael@0 | 1324 | let region = regions[i]; |
michael@0 | 1325 | if (region[0] > end) { |
michael@0 | 1326 | // First index is the beginning of match |
michael@0 | 1327 | boundaries.push(start); |
michael@0 | 1328 | // Second index is the beginning of non-match |
michael@0 | 1329 | boundaries.push(end); |
michael@0 | 1330 | |
michael@0 | 1331 | // Track the new region now that we've stored the previous one |
michael@0 | 1332 | start = region[0]; |
michael@0 | 1333 | } |
michael@0 | 1334 | |
michael@0 | 1335 | // Push back the end index for the current or new region |
michael@0 | 1336 | end = Math.max(end, region[1]); |
michael@0 | 1337 | } |
michael@0 | 1338 | |
michael@0 | 1339 | // Add the last region |
michael@0 | 1340 | boundaries.push(start); |
michael@0 | 1341 | boundaries.push(end); |
michael@0 | 1342 | |
michael@0 | 1343 | // Put on the end boundary if necessary |
michael@0 | 1344 | if (end < aText.length) |
michael@0 | 1345 | boundaries.push(aText.length); |
michael@0 | 1346 | |
michael@0 | 1347 | // Skip the first item because it's always 0 |
michael@0 | 1348 | return boundaries.slice(1); |
michael@0 | 1349 | ]]> |
michael@0 | 1350 | </body> |
michael@0 | 1351 | </method> |
michael@0 | 1352 | |
michael@0 | 1353 | <method name="_getSearchTokens"> |
michael@0 | 1354 | <parameter name="aSearch"/> |
michael@0 | 1355 | <body> |
michael@0 | 1356 | <![CDATA[ |
michael@0 | 1357 | let search = aSearch.toLowerCase(); |
michael@0 | 1358 | return search.split(/\s+/); |
michael@0 | 1359 | ]]> |
michael@0 | 1360 | </body> |
michael@0 | 1361 | </method> |
michael@0 | 1362 | |
michael@0 | 1363 | <method name="_setUpDescription"> |
michael@0 | 1364 | <parameter name="aDescriptionElement"/> |
michael@0 | 1365 | <parameter name="aText"/> |
michael@0 | 1366 | <parameter name="aNoEmphasis"/> |
michael@0 | 1367 | <body> |
michael@0 | 1368 | <![CDATA[ |
michael@0 | 1369 | // Get rid of all previous text |
michael@0 | 1370 | while (aDescriptionElement.hasChildNodes()) |
michael@0 | 1371 | aDescriptionElement.removeChild(aDescriptionElement.firstChild); |
michael@0 | 1372 | |
michael@0 | 1373 | // If aNoEmphasis is specified, don't add any emphasis |
michael@0 | 1374 | if (aNoEmphasis) { |
michael@0 | 1375 | aDescriptionElement.appendChild(document.createTextNode(aText)); |
michael@0 | 1376 | return; |
michael@0 | 1377 | } |
michael@0 | 1378 | |
michael@0 | 1379 | // Get the indices that separate match and non-match text |
michael@0 | 1380 | let search = this.getAttribute("text"); |
michael@0 | 1381 | let tokens = this._getSearchTokens(search); |
michael@0 | 1382 | let indices = this._getBoundaryIndices(aText, tokens); |
michael@0 | 1383 | |
michael@0 | 1384 | let next; |
michael@0 | 1385 | let start = 0; |
michael@0 | 1386 | let len = indices.length; |
michael@0 | 1387 | // Even indexed boundaries are matches, so skip the 0th if it's empty |
michael@0 | 1388 | for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) { |
michael@0 | 1389 | next = indices[i]; |
michael@0 | 1390 | let text = aText.substr(start, next - start); |
michael@0 | 1391 | start = next; |
michael@0 | 1392 | |
michael@0 | 1393 | if (i % 2 == 0) { |
michael@0 | 1394 | // Emphasize the text for even indices |
michael@0 | 1395 | let span = aDescriptionElement.appendChild( |
michael@0 | 1396 | document.createElementNS("http://www.w3.org/1999/xhtml", "span")); |
michael@0 | 1397 | span.className = "ac-emphasize-text"; |
michael@0 | 1398 | span.textContent = text; |
michael@0 | 1399 | } else { |
michael@0 | 1400 | // Otherwise, it's plain text |
michael@0 | 1401 | aDescriptionElement.appendChild(document.createTextNode(text)); |
michael@0 | 1402 | } |
michael@0 | 1403 | } |
michael@0 | 1404 | ]]> |
michael@0 | 1405 | </body> |
michael@0 | 1406 | </method> |
michael@0 | 1407 | |
michael@0 | 1408 | <method name="_adjustAcItem"> |
michael@0 | 1409 | <body> |
michael@0 | 1410 | <![CDATA[ |
michael@0 | 1411 | var url = this.getAttribute("url"); |
michael@0 | 1412 | var title = this.getAttribute("title"); |
michael@0 | 1413 | var type = this.getAttribute("type"); |
michael@0 | 1414 | |
michael@0 | 1415 | this.removeAttribute("actiontype"); |
michael@0 | 1416 | |
michael@0 | 1417 | // The ellipses are hidden via their visibility so that they always |
michael@0 | 1418 | // take up space and don't pop in on top of text when shown. For |
michael@0 | 1419 | // keyword searches, however, the title ellipsis should not take up |
michael@0 | 1420 | // space when hidden. Setting the hidden property accomplishes that. |
michael@0 | 1421 | this._titleOverflowEllipsis.hidden = false; |
michael@0 | 1422 | |
michael@0 | 1423 | // If the type includes an action, set up the item appropriately. |
michael@0 | 1424 | var types = type.split(/\s+/); |
michael@0 | 1425 | var actionIndex = types.indexOf("action"); |
michael@0 | 1426 | if (actionIndex >= 0) { |
michael@0 | 1427 | let [,action, param] = url.match(/^moz-action:([^,]+),(.*)$/); |
michael@0 | 1428 | this.setAttribute("actiontype", action); |
michael@0 | 1429 | url = param; |
michael@0 | 1430 | let desc = "]]>&action.switchToTab.label;<![CDATA["; |
michael@0 | 1431 | this._setUpDescription(this._action, desc, true); |
michael@0 | 1432 | |
michael@0 | 1433 | // Remove the "action" substring so that the correct style, if any, |
michael@0 | 1434 | // is applied below. |
michael@0 | 1435 | types.splice(actionIndex, 1); |
michael@0 | 1436 | type = types.join(" "); |
michael@0 | 1437 | } |
michael@0 | 1438 | |
michael@0 | 1439 | // If we have a tag match, show the tags and icon |
michael@0 | 1440 | if (type == "tag") { |
michael@0 | 1441 | // Configure the extra box for tags display |
michael@0 | 1442 | this._extraBox.hidden = false; |
michael@0 | 1443 | this._extraBox.childNodes[0].hidden = false; |
michael@0 | 1444 | this._extraBox.childNodes[1].hidden = true; |
michael@0 | 1445 | this._extraBox.pack = "end"; |
michael@0 | 1446 | this._titleBox.flex = 1; |
michael@0 | 1447 | |
michael@0 | 1448 | // The title is separated from the tags by an endash |
michael@0 | 1449 | let tags; |
michael@0 | 1450 | [, title, tags] = title.match(/^(.+) \u2013 (.+)$/); |
michael@0 | 1451 | |
michael@0 | 1452 | // Each tag is split by a comma in an undefined order, so sort it |
michael@0 | 1453 | let sortedTags = tags.split(",").sort().join(", "); |
michael@0 | 1454 | |
michael@0 | 1455 | // Emphasize the matching text in the tags |
michael@0 | 1456 | this._setUpDescription(this._extra, sortedTags); |
michael@0 | 1457 | |
michael@0 | 1458 | // Treat tagged matches as bookmarks for the star |
michael@0 | 1459 | type = "bookmark"; |
michael@0 | 1460 | } else if (type == "keyword") { |
michael@0 | 1461 | // Configure the extra box for keyword display |
michael@0 | 1462 | this._extraBox.hidden = false; |
michael@0 | 1463 | this._extraBox.childNodes[0].hidden = true; |
michael@0 | 1464 | this._extraBox.childNodes[1].hidden = false; |
michael@0 | 1465 | this._extraBox.pack = "start"; |
michael@0 | 1466 | this._titleBox.flex = 0; |
michael@0 | 1467 | |
michael@0 | 1468 | // Hide the ellipsis so it doesn't take up space. |
michael@0 | 1469 | this._titleOverflowEllipsis.hidden = true; |
michael@0 | 1470 | |
michael@0 | 1471 | // Put the parameters next to the title if we have any |
michael@0 | 1472 | let search = this.getAttribute("text"); |
michael@0 | 1473 | let params = ""; |
michael@0 | 1474 | let paramsIndex = search.indexOf(' '); |
michael@0 | 1475 | if (paramsIndex != -1) |
michael@0 | 1476 | params = search.substr(paramsIndex + 1); |
michael@0 | 1477 | |
michael@0 | 1478 | // Emphasize the keyword parameters |
michael@0 | 1479 | this._setUpDescription(this._extra, params); |
michael@0 | 1480 | |
michael@0 | 1481 | // Don't emphasize keyword searches in the title or url |
michael@0 | 1482 | this.setAttribute("text", ""); |
michael@0 | 1483 | } else { |
michael@0 | 1484 | // Hide the title's extra box if we don't need extra stuff |
michael@0 | 1485 | this._extraBox.hidden = true; |
michael@0 | 1486 | this._titleBox.flex = 1; |
michael@0 | 1487 | } |
michael@0 | 1488 | |
michael@0 | 1489 | // Give the image the icon style and a special one for the type |
michael@0 | 1490 | this._typeImage.className = "ac-type-icon" + |
michael@0 | 1491 | (type ? " ac-result-type-" + type : ""); |
michael@0 | 1492 | |
michael@0 | 1493 | // Show the url as the title if we don't have a title |
michael@0 | 1494 | if (title == "") |
michael@0 | 1495 | title = url; |
michael@0 | 1496 | |
michael@0 | 1497 | // Emphasize the matching search terms for the description |
michael@0 | 1498 | this._setUpDescription(this._title, title); |
michael@0 | 1499 | this._setUpDescription(this._url, url); |
michael@0 | 1500 | |
michael@0 | 1501 | // Set up overflow on a timeout because the contents of the box |
michael@0 | 1502 | // might not have a width yet even though we just changed them |
michael@0 | 1503 | setTimeout(this._setUpOverflow, 0, this._titleBox, this._titleOverflowEllipsis); |
michael@0 | 1504 | setTimeout(this._setUpOverflow, 0, this._urlBox, this._urlOverflowEllipsis); |
michael@0 | 1505 | ]]> |
michael@0 | 1506 | </body> |
michael@0 | 1507 | </method> |
michael@0 | 1508 | |
michael@0 | 1509 | <method name="_setUpOverflow"> |
michael@0 | 1510 | <parameter name="aParentBox"/> |
michael@0 | 1511 | <parameter name="aEllipsis"/> |
michael@0 | 1512 | <body> |
michael@0 | 1513 | <![CDATA[ |
michael@0 | 1514 | // Hide the ellipsis incase there's just enough to not underflow |
michael@0 | 1515 | aEllipsis.style.visibility = "hidden"; |
michael@0 | 1516 | |
michael@0 | 1517 | // Start with the parent's width and subtract off its children |
michael@0 | 1518 | let tooltip = []; |
michael@0 | 1519 | let children = aParentBox.childNodes; |
michael@0 | 1520 | let widthDiff = aParentBox.boxObject.width; |
michael@0 | 1521 | |
michael@0 | 1522 | for (let i = 0; i < children.length; i++) { |
michael@0 | 1523 | // Only consider a child if it actually takes up space |
michael@0 | 1524 | let childWidth = children[i].boxObject.width; |
michael@0 | 1525 | if (childWidth > 0) { |
michael@0 | 1526 | // Subtract a little less to account for subpixel rounding |
michael@0 | 1527 | widthDiff -= childWidth - .5; |
michael@0 | 1528 | |
michael@0 | 1529 | // Add to the tooltip if it's not hidden and has text |
michael@0 | 1530 | let childText = children[i].textContent; |
michael@0 | 1531 | if (childText) |
michael@0 | 1532 | tooltip.push(childText); |
michael@0 | 1533 | } |
michael@0 | 1534 | } |
michael@0 | 1535 | |
michael@0 | 1536 | // If the children take up more space than the parent.. overflow! |
michael@0 | 1537 | if (widthDiff < 0) { |
michael@0 | 1538 | // Re-show the ellipsis now that we know it's needed |
michael@0 | 1539 | aEllipsis.style.visibility = "visible"; |
michael@0 | 1540 | |
michael@0 | 1541 | // Separate text components with a ndash -- |
michael@0 | 1542 | aParentBox.tooltipText = tooltip.join(" \u2013 "); |
michael@0 | 1543 | } |
michael@0 | 1544 | ]]> |
michael@0 | 1545 | </body> |
michael@0 | 1546 | </method> |
michael@0 | 1547 | |
michael@0 | 1548 | <method name="_doUnderflow"> |
michael@0 | 1549 | <parameter name="aName"/> |
michael@0 | 1550 | <body> |
michael@0 | 1551 | <![CDATA[ |
michael@0 | 1552 | // Hide the ellipsis right when we know we're underflowing instead of |
michael@0 | 1553 | // waiting for the timeout to trigger the _setUpOverflow calculations |
michael@0 | 1554 | this[aName + "Box"].tooltipText = ""; |
michael@0 | 1555 | this[aName + "OverflowEllipsis"].style.visibility = "hidden"; |
michael@0 | 1556 | ]]> |
michael@0 | 1557 | </body> |
michael@0 | 1558 | </method> |
michael@0 | 1559 | |
michael@0 | 1560 | <method name="_doOverflow"> |
michael@0 | 1561 | <parameter name="aName"/> |
michael@0 | 1562 | <body> |
michael@0 | 1563 | <![CDATA[ |
michael@0 | 1564 | this._setUpOverflow(this[aName + "Box"], |
michael@0 | 1565 | this[aName + "OverflowEllipsis"]); |
michael@0 | 1566 | ]]> |
michael@0 | 1567 | </body> |
michael@0 | 1568 | </method> |
michael@0 | 1569 | |
michael@0 | 1570 | </implementation> |
michael@0 | 1571 | </binding> |
michael@0 | 1572 | |
michael@0 | 1573 | <binding id="autocomplete-tree" extends="chrome://global/content/bindings/tree.xml#tree"> |
michael@0 | 1574 | <content> |
michael@0 | 1575 | <children includes="treecols"/> |
michael@0 | 1576 | <xul:treerows class="autocomplete-treerows tree-rows" xbl:inherits="hidescrollbar" flex="1"> |
michael@0 | 1577 | <children/> |
michael@0 | 1578 | </xul:treerows> |
michael@0 | 1579 | </content> |
michael@0 | 1580 | </binding> |
michael@0 | 1581 | |
michael@0 | 1582 | <binding id="autocomplete-richlistbox" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox"> |
michael@0 | 1583 | <implementation> |
michael@0 | 1584 | <field name="mLastMoveTime">Date.now()</field> |
michael@0 | 1585 | </implementation> |
michael@0 | 1586 | <handlers> |
michael@0 | 1587 | <handler event="mouseup"> |
michael@0 | 1588 | <![CDATA[ |
michael@0 | 1589 | // don't call onPopupClick for the scrollbar buttons, thumb, slider, etc. |
michael@0 | 1590 | var item = event.originalTarget; |
michael@0 | 1591 | |
michael@0 | 1592 | while (item && item.localName != "richlistitem") |
michael@0 | 1593 | item = item.parentNode; |
michael@0 | 1594 | |
michael@0 | 1595 | if (!item) |
michael@0 | 1596 | return; |
michael@0 | 1597 | |
michael@0 | 1598 | this.parentNode.onPopupClick(event); |
michael@0 | 1599 | ]]> |
michael@0 | 1600 | </handler> |
michael@0 | 1601 | |
michael@0 | 1602 | <handler event="mousemove"> |
michael@0 | 1603 | <![CDATA[ |
michael@0 | 1604 | if (Date.now() - this.mLastMoveTime > 30) { |
michael@0 | 1605 | var item = event.target; |
michael@0 | 1606 | |
michael@0 | 1607 | while (item && item.localName != "richlistitem") |
michael@0 | 1608 | item = item.parentNode; |
michael@0 | 1609 | |
michael@0 | 1610 | if (!item) |
michael@0 | 1611 | return; |
michael@0 | 1612 | |
michael@0 | 1613 | var rc = this.getIndexOfItem(item); |
michael@0 | 1614 | if (rc != this.selectedIndex) |
michael@0 | 1615 | this.selectedIndex = rc; |
michael@0 | 1616 | |
michael@0 | 1617 | this.mLastMoveTime = Date.now(); |
michael@0 | 1618 | } |
michael@0 | 1619 | ]]> |
michael@0 | 1620 | </handler> |
michael@0 | 1621 | </handlers> |
michael@0 | 1622 | </binding> |
michael@0 | 1623 | |
michael@0 | 1624 | <binding id="autocomplete-treebody"> |
michael@0 | 1625 | <implementation> |
michael@0 | 1626 | <field name="mLastMoveTime">Date.now()</field> |
michael@0 | 1627 | </implementation> |
michael@0 | 1628 | |
michael@0 | 1629 | <handlers> |
michael@0 | 1630 | <handler event="mouseup" action="this.parentNode.parentNode.onPopupClick(event);"/> |
michael@0 | 1631 | |
michael@0 | 1632 | <handler event="mousedown"><![CDATA[ |
michael@0 | 1633 | var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY); |
michael@0 | 1634 | if (rc != this.parentNode.currentIndex) |
michael@0 | 1635 | this.parentNode.view.selection.select(rc); |
michael@0 | 1636 | ]]></handler> |
michael@0 | 1637 | |
michael@0 | 1638 | <handler event="mousemove"><![CDATA[ |
michael@0 | 1639 | if (Date.now() - this.mLastMoveTime > 30) { |
michael@0 | 1640 | var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY); |
michael@0 | 1641 | if (rc != this.parentNode.currentIndex) |
michael@0 | 1642 | this.parentNode.view.selection.select(rc); |
michael@0 | 1643 | this.mLastMoveTime = Date.now(); |
michael@0 | 1644 | } |
michael@0 | 1645 | ]]></handler> |
michael@0 | 1646 | </handlers> |
michael@0 | 1647 | </binding> |
michael@0 | 1648 | |
michael@0 | 1649 | <binding id="autocomplete-treerows"> |
michael@0 | 1650 | <content> |
michael@0 | 1651 | <xul:hbox flex="1" class="tree-bodybox"> |
michael@0 | 1652 | <children/> |
michael@0 | 1653 | </xul:hbox> |
michael@0 | 1654 | <xul:scrollbar xbl:inherits="collapsed=hidescrollbar" orient="vertical" class="tree-scrollbar"/> |
michael@0 | 1655 | </content> |
michael@0 | 1656 | </binding> |
michael@0 | 1657 | |
michael@0 | 1658 | <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker"> |
michael@0 | 1659 | <implementation> |
michael@0 | 1660 | <method name="showPopup"> |
michael@0 | 1661 | <body><![CDATA[ |
michael@0 | 1662 | var textbox = document.getBindingParent(this); |
michael@0 | 1663 | textbox.showHistoryPopup(); |
michael@0 | 1664 | ]]></body> |
michael@0 | 1665 | </method> |
michael@0 | 1666 | </implementation> |
michael@0 | 1667 | |
michael@0 | 1668 | <handlers> |
michael@0 | 1669 | <handler event="mousedown" button="0"><![CDATA[ |
michael@0 | 1670 | this.showPopup(); |
michael@0 | 1671 | ]]></handler> |
michael@0 | 1672 | </handlers> |
michael@0 | 1673 | </binding> |
michael@0 | 1674 | |
michael@0 | 1675 | </bindings> |