toolkit/content/widgets/autocomplete.xml

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:30fc0411fec9
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>

mercurial