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