Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
"use strict";
6
7
var EXPORTED_SYMBOLS = ["UrlbarInput"];
8
9
const { XPCOMUtils } = ChromeUtils.import(
11
);
12
13
XPCOMUtils.defineLazyModuleGetters(this, {
21
UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.jsm",
23
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
24
UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm",
27
UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.jsm",
29
});
30
31
XPCOMUtils.defineLazyServiceGetter(
32
this,
33
"ClipboardHelper",
34
"@mozilla.org/widget/clipboardhelper;1",
35
"nsIClipboardHelper"
36
);
37
38
const SEARCH_BUTTON_ID = "urlbar-search-button";
39
40
let getBoundsWithoutFlushing = element =>
41
element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
42
let px = number => number.toFixed(2) + "px";
43
44
/**
45
* Implements the text input part of the address bar UI.
46
*/
47
class UrlbarInput {
48
/**
49
* @param {object} options
50
* The initial options for UrlbarInput.
51
* @param {object} options.textbox
52
* The container element.
53
*/
54
constructor(options = {}) {
55
this.textbox = options.textbox;
56
57
this.window = this.textbox.ownerGlobal;
58
this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.window);
59
this.document = this.window.document;
60
this.window.addEventListener("unload", this);
61
62
// Create the panel to contain results.
63
this.textbox.appendChild(
64
this.window.MozXULElement.parseXULToFragment(`
65
<vbox class="urlbarView"
66
role="group"
67
tooltip="aHTMLTooltip"
68
context="">
69
<html:div class="urlbarView-body-outer">
70
<html:div class="urlbarView-body-inner">
71
<html:div id="urlbar-results"
72
class="urlbarView-results"
73
role="listbox"/>
74
</html:div>
75
</html:div>
76
<hbox class="search-one-offs"
77
compact="true"
78
includecurrentengine="true"
79
disabletab="true"/>
80
</vbox>
81
`)
82
);
83
this.panel = this.textbox.querySelector(".urlbarView");
84
85
// "Megabar" is the internal codename for the update1 design refresh.
86
this.megabar = UrlbarPrefs.get("update1");
87
if (this.megabar) {
88
this.textbox.classList.add("megabar");
89
this.textbox.parentNode.classList.add("megabar");
90
this.searchButton = UrlbarPrefs.get("update2.searchButton");
91
if (this.searchButton) {
92
this.textbox.classList.add("searchButton");
93
}
94
}
95
96
this.controller = new UrlbarController({
97
input: this,
98
eventTelemetryCategory: options.eventTelemetryCategory,
99
});
100
this.view = new UrlbarView(this);
101
this.valueIsTyped = false;
102
this.lastQueryContextPromise = Promise.resolve();
103
this._actionOverrideKeyCount = 0;
104
this._autofillPlaceholder = "";
105
this._lastSearchString = "";
106
this._valueOnLastSearch = "";
107
this._resultForCurrentValue = null;
108
this._suppressStartQuery = false;
109
this._suppressPrimaryAdjustment = false;
110
this._untrimmedValue = "";
111
112
// This exists only for tests.
113
this._enableAutofillPlaceholder = true;
114
115
// Forward certain methods and properties.
116
const CONTAINER_METHODS = [
117
"getAttribute",
118
"hasAttribute",
119
"querySelector",
120
"setAttribute",
121
"removeAttribute",
122
"toggleAttribute",
123
];
124
const INPUT_METHODS = [
125
"addEventListener",
126
"blur",
127
"focus",
128
"removeEventListener",
129
];
130
const READ_WRITE_PROPERTIES = [
131
"placeholder",
132
"readOnly",
133
"selectionStart",
134
"selectionEnd",
135
];
136
137
for (let method of CONTAINER_METHODS) {
138
this[method] = (...args) => {
139
return this.textbox[method](...args);
140
};
141
}
142
143
for (let method of INPUT_METHODS) {
144
this[method] = (...args) => {
145
return this.inputField[method](...args);
146
};
147
}
148
149
for (let property of READ_WRITE_PROPERTIES) {
150
Object.defineProperty(this, property, {
151
enumerable: true,
152
get() {
153
return this.inputField[property];
154
},
155
set(val) {
156
return (this.inputField[property] = val);
157
},
158
});
159
}
160
161
this.inputField = this.querySelector("#urlbar-input");
162
this.dropmarker = this.querySelector(".urlbar-history-dropmarker");
163
this._inputContainer = this.querySelector("#urlbar-input-container");
164
this._identityBox = this.querySelector("#identity-box");
165
this._toolbar = this.textbox.closest("toolbar");
166
167
XPCOMUtils.defineLazyGetter(this, "valueFormatter", () => {
168
return new UrlbarValueFormatter(this);
169
});
170
171
// If the toolbar is not visible in this window or the urlbar is readonly,
172
// we'll stop here, so that most properties of the input object are valid,
173
// but we won't handle events.
174
if (!this.window.toolbar.visible || this.readOnly) {
175
return;
176
}
177
178
// The event bufferer can be used to defer events that may affect users
179
// muscle memory; for example quickly pressing DOWN+ENTER should end up
180
// on a predictable result, regardless of the search status. The event
181
// bufferer will invoke the handling code at the right time.
182
this.eventBufferer = new UrlbarEventBufferer(this);
183
184
this._inputFieldEvents = [
185
"compositionstart",
186
"compositionend",
187
"contextmenu",
188
"dragover",
189
"dragstart",
190
"drop",
191
"focus",
192
"blur",
193
"input",
194
"keydown",
195
"keyup",
196
"mouseover",
197
"overflow",
198
"underflow",
199
"paste",
200
"scrollend",
201
"select",
202
];
203
for (let name of this._inputFieldEvents) {
204
this.addEventListener(name, this);
205
}
206
207
this.dropmarker.addEventListener("mousedown", this);
208
this.window.addEventListener("mousedown", this);
209
this.textbox.addEventListener("mousedown", this);
210
this._inputContainer.addEventListener("click", this);
211
212
// This is used to detect commands launched from the panel, to avoid
213
// recording abandonment events when the command causes a blur event.
214
this.view.panel.addEventListener("command", this, true);
215
216
this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
217
218
this.window.addEventListener("customizationstarting", this);
219
this.window.addEventListener("aftercustomization", this);
220
221
this.updateLayoutBreakout();
222
223
this._initCopyCutController();
224
this._initPasteAndGo();
225
226
// Tracks IME composition.
227
this._compositionState = UrlbarUtils.COMPOSITION.NONE;
228
this._compositionClosedPopup = false;
229
230
this.editor.QueryInterface(Ci.nsIPlaintextEditor).newlineHandling =
231
Ci.nsIPlaintextEditor.eNewlinesStripSurroundingWhitespace;
232
233
this._setOpenViewOnFocus();
234
Services.prefs.addObserver("browser.urlbar.openViewOnFocus", this);
235
}
236
237
/**
238
* Applies styling to the text in the urlbar input, depending on the text.
239
*/
240
formatValue() {
241
// The editor may not exist if the toolbar is not visible.
242
if (this.editor) {
243
this.valueFormatter.update();
244
}
245
}
246
247
select() {
248
// See _on_select(). HTMLInputElement.select() dispatches a "select"
249
// event but does not set the primary selection.
250
this._suppressPrimaryAdjustment = true;
251
this.inputField.select();
252
this._suppressPrimaryAdjustment = false;
253
}
254
255
/**
256
* Converts an internal URI (e.g. a URI with a username or password) into one
257
* which we can expose to the user.
258
*
259
* @param {nsIURI} uri
260
* The URI to be converted
261
* @returns {nsIURI}
262
* The converted, exposable URI
263
*/
264
makeURIReadable(uri) {
265
// Avoid copying 'about:reader?url=', and always provide the original URI:
266
// Reader mode ensures we call createExposableURI itself.
267
let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay(
268
uri.displaySpec
269
);
270
if (readerStrippedURI) {
271
return readerStrippedURI;
272
}
273
274
try {
275
return Services.uriFixup.createExposableURI(uri);
276
} catch (ex) {}
277
278
return uri;
279
}
280
281
observe(subject, topic, data) {
282
switch (data) {
283
case "browser.urlbar.openViewOnFocus":
284
this._setOpenViewOnFocus();
285
break;
286
}
287
}
288
289
/**
290
* Passes DOM events to the _on_<event type> methods.
291
* @param {Event} event
292
*/
293
handleEvent(event) {
294
let methodName = "_on_" + event.type;
295
if (methodName in this) {
296
this[methodName](event);
297
} else {
298
throw new Error("Unrecognized UrlbarInput event: " + event.type);
299
}
300
}
301
302
/**
303
* Handles an event which would cause a url or text to be opened.
304
*
305
* @param {Event} [event] The event triggering the open.
306
* @param {string} [openWhere] Where we expect the result to be opened.
307
* @param {object} [openParams]
308
* The parameters related to where the result will be opened.
309
* @param {object} [triggeringPrincipal]
310
* The principal that the action was triggered from.
311
*/
312
handleCommand(event, openWhere, openParams = {}, triggeringPrincipal = null) {
313
let isMouseEvent = event instanceof this.window.MouseEvent;
314
if (isMouseEvent && event.button == 2) {
315
// Do nothing for right clicks.
316
return;
317
}
318
319
// Determine whether to use the selected one-off search button. In
320
// one-off search buttons parlance, "selected" means that the button
321
// has been navigated to via the keyboard. So we want to use it if
322
// the triggering event is not a mouse click -- i.e., it's a Return
323
// key -- or if the one-off was mouse-clicked.
324
let selectedOneOff;
325
if (this.view.isOpen) {
326
selectedOneOff = this.view.oneOffSearchButtons.selectedButton;
327
if (selectedOneOff && isMouseEvent && event.target != selectedOneOff) {
328
selectedOneOff = null;
329
}
330
// Do the command of the selected one-off if it's not an engine.
331
if (selectedOneOff && !selectedOneOff.engine) {
332
this.controller.engagementEvent.discard();
333
selectedOneOff.doCommand();
334
return;
335
}
336
}
337
338
// Use the selected element if we have one; this is usually the case
339
// when the view is open.
340
let element = this.view.selectedElement;
341
let result = this.view.getResultFromElement(element);
342
let selectedPrivateResult =
343
result &&
344
result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
345
result.payload.inPrivateWindow;
346
let selectedPrivateEngineResult =
347
selectedPrivateResult && result.payload.isPrivateEngine;
348
if (element && (!selectedOneOff || selectedPrivateEngineResult)) {
349
this.pickElement(element, event);
350
return;
351
}
352
353
let url;
354
let selType = this.controller.engagementEvent.typeFromElement(element);
355
let numChars = this.value.length;
356
if (selectedOneOff) {
357
selType = "oneoff";
358
numChars = this._lastSearchString.length;
359
// If there's a selected one-off button then load a search using
360
// the button's engine.
361
result = this._resultForCurrentValue;
362
let searchString =
363
(result && (result.payload.suggestion || result.payload.query)) ||
364
this._lastSearchString;
365
[url, openParams.postData] = UrlbarUtils.getSearchQueryUrl(
366
selectedOneOff.engine,
367
searchString
368
);
369
this._recordSearch(selectedOneOff.engine, event);
370
} else {
371
// Use the current value if we don't have a UrlbarResult e.g. because the
372
// view is closed.
373
url = this.untrimmedValue;
374
openParams.postData = null;
375
}
376
377
if (!url) {
378
return;
379
}
380
381
this.controller.recordSelectedResult(
382
event,
383
result || this.view.selectedResult
384
);
385
386
let where = openWhere || this._whereToOpen(event);
387
if (selectedPrivateResult) {
388
where = "window";
389
openParams.private = true;
390
}
391
openParams.allowInheritPrincipal = false;
392
url = this._maybeCanonizeURL(event, url) || url.trim();
393
394
this.controller.engagementEvent.record(event, {
395
numChars,
396
selIndex: this.view.selectedRowIndex,
397
selType,
398
});
399
400
try {
401
new URL(url);
402
} catch (ex) {
403
// This is not a URL, so it must be a search or a keyword.
404
405
// TODO (Bug 1604927): If the urlbar results are restricted to a specific
406
// engine, here we must search with that specific engine; indeed the
407
// docshell wouldn't know about our engine restriction.
408
// Also remember to invoke this._recordSearch, after replacing url with
409
// the appropriate engine submission url.
410
411
let browser = this.window.gBrowser.selectedBrowser;
412
let lastLocationChange = browser.lastLocationChange;
413
UrlbarUtils.getShortcutOrURIAndPostData(url).then(data => {
414
// Because this happens asynchronously, we must verify that the browser
415
// location did not change in the meanwhile.
416
if (
417
where != "current" ||
418
browser.lastLocationChange == lastLocationChange
419
) {
420
openParams.postData = data.postData;
421
openParams.allowInheritPrincipal = data.mayInheritPrincipal;
422
this._loadURL(data.url, where, openParams, null, browser);
423
}
424
});
425
// Bail out, because we will handle the _loadURL call asynchronously.
426
return;
427
}
428
429
this._loadURL(url, where, openParams);
430
}
431
432
handleRevert() {
433
this.window.gBrowser.userTypedValue = null;
434
this.window.URLBarSetURI(null, true);
435
if (this.value && this.focused) {
436
this.select();
437
}
438
}
439
440
/**
441
* Called by the view when an element is picked.
442
*
443
* @param {Element} element The element that was picked.
444
* @param {Event} event The event that picked the element.
445
*/
446
pickElement(element, event) {
447
let originalUntrimmedValue = this.untrimmedValue;
448
let result = this.view.getResultFromElement(element);
449
if (!result) {
450
return;
451
}
452
let isCanonized = this.setValueFromResult(result, event);
453
let where = this._whereToOpen(event);
454
let openParams = {
455
allowInheritPrincipal: false,
456
};
457
458
let selIndex = this.view.selectedRowIndex;
459
if (!result.payload.keywordOffer) {
460
this.view.close();
461
}
462
463
this.controller.recordSelectedResult(event, result);
464
465
if (isCanonized) {
466
this.controller.engagementEvent.record(event, {
467
numChars: this._lastSearchString.length,
468
selIndex,
469
selType: "canonized",
470
});
471
this._loadURL(this.value, where, openParams);
472
return;
473
}
474
475
let { url, postData } = UrlbarUtils.getUrlFromResult(result);
476
openParams.postData = postData;
477
478
switch (result.type) {
479
case UrlbarUtils.RESULT_TYPE.URL: {
480
// Bug 1578856: both the provider and the docshell run heuristics to
481
// decide how to handle a non-url string, either fixing it to a url, or
482
// searching for it.
483
// Some preferences can control the docshell behavior, for example
484
// if dns_first_for_single_words is true, the docshell looks up the word
485
// against the dns server, and either loads it as an url or searches for
486
// it, depending on the lookup result. The provider instead will always
487
// return a fixed url in this case, because URIFixup is synchronous and
488
// can't do a synchronous dns lookup. A possible long term solution
489
// would involve sharing the docshell logic with the provider, along
490
// with the dns lookup.
491
// For now, in this specific case, we'll override the result's url
492
// with the input value, and let it pass through to _loadURL(), and
493
// finally to the docshell.
494
// This also means that in some cases the heuristic result will show a
495
// Visit entry, but the docshell will instead execute a search. It's a
496
// rare case anyway, most likely to happen for enterprises customizing
497
// the urifixup prefs.
498
if (
499
result.heuristic &&
500
UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
501
UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
502
) {
503
url = originalUntrimmedValue;
504
}
505
break;
506
}
507
case UrlbarUtils.RESULT_TYPE.KEYWORD: {
508
// If this result comes from a bookmark keyword, let it inherit the
509
// current document's principal, otherwise bookmarklets would break.
510
openParams.allowInheritPrincipal = true;
511
break;
512
}
513
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
514
if (this.hasAttribute("actionoverride")) {
515
where = "current";
516
break;
517
}
518
519
this.handleRevert();
520
let prevTab = this.window.gBrowser.selectedTab;
521
let loadOpts = {
522
adoptIntoActiveWindow: UrlbarPrefs.get(
523
"switchTabs.adoptIntoActiveWindow"
524
),
525
};
526
527
this.controller.engagementEvent.record(event, {
528
numChars: this._lastSearchString.length,
529
selIndex,
530
selType: "tabswitch",
531
});
532
533
let switched = this.window.switchToTabHavingURI(
534
Services.io.newURI(url),
535
false,
536
loadOpts
537
);
538
if (switched && prevTab.isEmpty) {
539
this.window.gBrowser.removeTab(prevTab);
540
}
541
return;
542
}
543
case UrlbarUtils.RESULT_TYPE.SEARCH: {
544
if (result.payload.keywordOffer) {
545
// The user confirmed a token alias, so just move the caret
546
// to the end of it. Because there's a trailing space in the value,
547
// the user can directly start typing a query string at that point.
548
this.selectionStart = this.selectionEnd = this.value.length;
549
550
this.controller.engagementEvent.record(event, {
551
numChars: this._lastSearchString.length,
552
selIndex,
553
selType: "keywordoffer",
554
});
555
556
// Picking a keyword offer just fills it in the input and doesn't
557
// visit anything. The user can then type a search string. Also
558
// start a new search so that the offer appears in the view by itself
559
// to make it even clearer to the user what's going on.
560
this.startQuery();
561
return;
562
}
563
564
if (
565
result.heuristic &&
566
UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
567
) {
568
// The docshell when fixes a single word to a search, also checks the
569
// dns and prompts the user whether they wanted to rather visit that
570
// as a host. On a positive answer, it adds to the domains whitelist
571
// that we use to make decisions. Because here we are directly asking
572
// for a search, bypassing the docshell, we must do it here.
573
// See URIFixupChild.jsm and keyword-uri-fixup.
574
let flags =
575
Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
576
Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
577
if (PrivateBrowsingUtils.isWindowPrivate(this.window)) {
578
flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
579
}
580
// Don't interrupt the load action in case of errors.
581
try {
582
let fixupInfo = Services.uriFixup.getFixupURIInfo(
583
originalUntrimmedValue.trim(),
584
flags
585
);
586
this.window.gKeywordURIFixup({
587
target: this.window.gBrowser.selectedBrowser,
588
data: fixupInfo,
589
});
590
} catch (ex) {
591
Cu.reportError(
592
`An error occured while trying to fixup "${originalUntrimmedValue.trim()}": ${ex}`
593
);
594
}
595
}
596
597
if (result.payload.inPrivateWindow) {
598
where = "window";
599
openParams.private = true;
600
}
601
602
const actionDetails = {
603
isSuggestion: !!result.payload.suggestion,
604
alias: result.payload.keyword,
605
};
606
const engine = Services.search.getEngineByName(result.payload.engine);
607
this._recordSearch(engine, event, actionDetails);
608
break;
609
}
610
case UrlbarUtils.RESULT_TYPE.TIP: {
611
if (element.classList.contains("urlbarView-tip-help")) {
612
url = result.payload.helpUrl;
613
}
614
if (!url) {
615
this.handleRevert();
616
this.controller.engagementEvent.record(event, {
617
numChars: this._lastSearchString.length,
618
selIndex,
619
selType: "tip",
620
});
621
let provider = UrlbarProvidersManager.getProvider(
622
result.providerName
623
);
624
if (!provider) {
625
Cu.reportError(`Provider not found: ${result.providerName}`);
626
return;
627
}
628
provider.pickResult(result);
629
return;
630
}
631
break;
632
}
633
case UrlbarUtils.RESULT_TYPE.OMNIBOX: {
634
this.controller.engagementEvent.record(event, {
635
numChars: this._lastSearchString.length,
636
selIndex,
637
selType: "extension",
638
});
639
640
// The urlbar needs to revert to the loaded url when a command is
641
// handled by the extension.
642
this.handleRevert();
643
// We don't directly handle a load when an Omnibox API result is picked,
644
// instead we forward the request to the WebExtension itself, because
645
// the value may not even be a url.
646
// We pass the keyword and content, that actually is the retrieved value
647
// prefixed by the keyword. ExtensionSearchHandler uses this keyword
648
// redundancy as a sanity check.
649
ExtensionSearchHandler.handleInputEntered(
650
result.payload.keyword,
651
result.payload.content,
652
where
653
);
654
return;
655
}
656
}
657
658
if (!url) {
659
throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
660
}
661
662
if (!this.isPrivate && !result.heuristic) {
663
// This should not interrupt the load anyway.
664
UrlbarUtils.addToInputHistory(url, this._lastSearchString).catch(
665
Cu.reportError
666
);
667
}
668
669
this.controller.engagementEvent.record(event, {
670
numChars: this._lastSearchString.length,
671
selIndex,
672
selType: this.controller.engagementEvent.typeFromElement(element),
673
});
674
675
this._loadURL(url, where, openParams, {
676
source: result.source,
677
type: result.type,
678
});
679
}
680
681
/**
682
* Called by the view when moving through results with the keyboard, and when
683
* picking a result.
684
*
685
* @param {UrlbarResult} [result]
686
* The result that was selected or picked, null if no result was selected.
687
* @param {Event} [event] The event that picked the result.
688
* @returns {boolean}
689
* Whether the value has been canonized
690
*/
691
setValueFromResult(result = null, event = null) {
692
let canonizedUrl;
693
694
if (!result) {
695
// This usually happens when there's no selected results (the user cycles
696
// through results and there was no heuristic), and we reset the input
697
// value to the previous text value.
698
this.value = this._valueOnLastSearch;
699
} else {
700
// For autofilled results, the value that should be canonized is not the
701
// autofilled value but the value that the user typed.
702
canonizedUrl = this._maybeCanonizeURL(
703
event,
704
result.autofill ? this._lastSearchString : this.value
705
);
706
if (canonizedUrl) {
707
this.value = canonizedUrl;
708
} else if (result.autofill) {
709
let { value, selectionStart, selectionEnd } = result.autofill;
710
this._autofillValue(value, selectionStart, selectionEnd);
711
} else {
712
this.value = this._getValueFromResult(result);
713
}
714
}
715
this._resultForCurrentValue = result;
716
717
// The value setter clobbers the actiontype attribute, so update this after
718
// that.
719
if (result) {
720
switch (result.type) {
721
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
722
this.setAttribute("actiontype", "switchtab");
723
break;
724
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
725
this.setAttribute("actiontype", "extension");
726
break;
727
}
728
}
729
730
return !!canonizedUrl;
731
}
732
733
/**
734
* Called by the controller when the first result of a new search is received.
735
* If it's an autofill result, then it may need to be autofilled, subject to a
736
* few restrictions.
737
*
738
* @param {UrlbarResult} result
739
* The first result.
740
*/
741
autofillFirstResult(result) {
742
if (!result.autofill) {
743
return;
744
}
745
746
let isPlaceholderSelected =
747
this.selectionEnd == this._autofillPlaceholder.length &&
748
this.selectionStart == this._lastSearchString.length &&
749
this._autofillPlaceholder
750
.toLocaleLowerCase()
751
.startsWith(this._lastSearchString.toLocaleLowerCase());
752
753
// Don't autofill if there's already a selection (with one caveat described
754
// next) or the cursor isn't at the end of the input. But if there is a
755
// selection and it's the autofill placeholder value, then do autofill.
756
if (
757
!isPlaceholderSelected &&
758
!this._autofillIgnoresSelection &&
759
(this.selectionStart != this.selectionEnd ||
760
this.selectionEnd != this._lastSearchString.length)
761
) {
762
return;
763
}
764
765
this.setValueFromResult(result);
766
}
767
768
/**
769
* Invoked by the view when the first result is received.
770
* To prevent selection flickering, we apply autofill on input through a
771
* placeholder, without waiting for results.
772
* But, if the first result is not an autofill one, the autofill prediction
773
* was wrong and we should restore the original user typed string.
774
* @param {UrlbarResult} firstResult The first result received.
775
*/
776
maybeClearAutofillPlaceholder(firstResult) {
777
if (
778
this._autofillPlaceholder &&
779
!firstResult.autofill &&
780
// Avoid clobbering added spaces (for token aliases, for example).
781
!this.value.endsWith(" ")
782
) {
783
this._setValue(this.window.gBrowser.userTypedValue, false);
784
}
785
}
786
787
/**
788
* Starts a query based on the current input value.
789
*
790
* @param {boolean} [options.allowAutofill]
791
* Whether or not to allow providers to include autofill results.
792
* @param {boolean} [options.autofillIgnoresSelection]
793
* Normally we autofill only if the cursor is at the end of the string,
794
* if this is set we'll autofill regardless of selection.
795
* @param {string} [options.searchString]
796
* The search string. If not given, the current input value is used.
797
* Otherwise, the current input value must start with this value.
798
* @param {boolean} [options.resetSearchState]
799
* If this is the first search of a user interaction with the input, set
800
* this to true (the default) so that search-related state from the previous
801
* interaction doesn't interfere with the new interaction. Otherwise set it
802
* to false so that state is maintained during a single interaction. The
803
* intended use for this parameter is that it should be set to false when
804
* this method is called due to input events.
805
* @param {event} [options.event]
806
* The user-generated event that triggered the query, if any. If given, we
807
* will record engagement event telemetry for the query.
808
*/
809
startQuery({
810
allowAutofill = true,
811
autofillIgnoresSelection = false,
812
searchString = null,
813
resetSearchState = true,
814
event = null,
815
} = {}) {
816
if (!searchString) {
817
searchString =
818
this.getAttribute("pageproxystate") == "valid" ? "" : this.value;
819
} else if (!this.value.startsWith(searchString)) {
820
throw new Error("The current value doesn't start with the search string");
821
}
822
823
if (event) {
824
this.controller.engagementEvent.start(event, searchString);
825
}
826
827
if (this._suppressStartQuery) {
828
return;
829
}
830
831
this._autofillIgnoresSelection = autofillIgnoresSelection;
832
if (resetSearchState) {
833
this._resetSearchState();
834
}
835
836
this._lastSearchString = searchString;
837
this._valueOnLastSearch = this.value;
838
839
// TODO (Bug 1522902): This promise is necessary for tests, because some
840
// tests are not listening for completion when starting a query through
841
// other methods than startQuery (input events for example).
842
this.lastQueryContextPromise = this.controller.startQuery(
843
new UrlbarQueryContext({
844
allowAutofill,
845
isPrivate: this.isPrivate,
846
maxResults: UrlbarPrefs.get("maxRichResults"),
847
searchString,
848
userContextId: this.window.gBrowser.selectedBrowser.getAttribute(
849
"usercontextid"
850
),
851
})
852
);
853
}
854
855
/**
856
* Sets the input's value, starts a search, and opens the view.
857
*
858
* @param {string} value
859
* The input's value will be set to this value, and the search will
860
* use it as its query.
861
* @param {boolean} [options.focus]
862
* If true, the urlbar will be focused. If false, the focus will remain
863
* unchanged.
864
*/
865
search(value, { focus = true } = {}) {
866
if (focus) {
867
this.window.focusAndSelectUrlBar();
868
}
869
870
// If the value is a restricted token, append a space.
871
if (Object.values(UrlbarTokenizer.RESTRICT).includes(value)) {
872
this.inputField.value = value + " ";
873
this._revertOnBlurValue = this.value;
874
} else {
875
this.inputField.value = value;
876
}
877
878
// Avoid selecting the text if this method is called twice in a row.
879
this.selectionStart = -1;
880
881
// Note: proper IME Composition handling depends on the fact this generates
882
// an input event, rather than directly invoking the controller; everything
883
// goes through _on_input, that will properly skip the search until the
884
// composition is committed. _on_input also skips the search when it's the
885
// same as the previous search, but we want to allow consecutive searches
886
// with the same string. So clear _lastSearchString first.
887
this._lastSearchString = "";
888
let event = this.document.createEvent("UIEvents");
889
event.initUIEvent("input", true, false, this.window, 0);
890
this.inputField.dispatchEvent(event);
891
}
892
893
/**
894
* Focus without the focus styles.
895
* This is used by Activity Stream and about:privatebrowsing for search hand-off.
896
*/
897
setHiddenFocus() {
898
this._hideFocus = true;
899
if (this.focused) {
900
this.removeAttribute("focused");
901
} else {
902
this.focus();
903
}
904
}
905
906
/**
907
* Restore focus styles.
908
* This is used by Activity Stream and about:privatebrowsing for search hand-off.
909
*/
910
removeHiddenFocus() {
911
this._hideFocus = false;
912
if (this.focused) {
913
this.setAttribute("focused", "true");
914
this.startLayoutExtend();
915
}
916
}
917
918
// Getters and Setters below.
919
920
get editor() {
921
return this.inputField.editor;
922
}
923
924
get focused() {
925
return this.document.activeElement == this.inputField;
926
}
927
928
get goButton() {
929
return this.querySelector("#urlbar-go-button");
930
}
931
932
get value() {
933
return this.inputField.value;
934
}
935
936
get untrimmedValue() {
937
return this._untrimmedValue;
938
}
939
940
set value(val) {
941
return this._setValue(val, true);
942
}
943
944
get lastSearchString() {
945
return this._lastSearchString;
946
}
947
948
get openViewOnFocus() {
949
return (
950
this._openViewOnFocus &&
951
!this.isPrivate &&
952
// We do not show Top Sites if the user disabled them on about:newtab. We
953
// handle this here instead of disabling UrlbarProviderTopSites so that
954
// the user can still show Top Sites with the down arrow.
955
Services.prefs.getBoolPref(
956
"browser.newtabpage.activity-stream.feeds.topsites",
957
true
958
)
959
);
960
}
961
962
async updateLayoutBreakout() {
963
if (!this.megabar) {
964
return;
965
}
966
await this._updateLayoutBreakoutDimensions();
967
this.startLayoutExtend();
968
}
969
970
startLayoutExtend() {
971
// Do not expand if:
972
// The Urlbar does not support being expanded or it is already expanded
973
if (
974
!this.hasAttribute("breakout") ||
975
this.hasAttribute("breakout-extend")
976
) {
977
return;
978
}
979
// The Urlbar is unfocused and the view is closed
980
if (this.getAttribute("focused") != "true" && !this.view.isOpen) {
981
return;
982
}
983
984
if (UrlbarPrefs.get("disableExtendForTests")) {
985
this.setAttribute("breakout-extend-disabled", "true");
986
return;
987
}
988
this.removeAttribute("breakout-extend-disabled");
989
990
if (this._toolbar) {
991
this._toolbar.setAttribute("urlbar-exceeds-toolbar-bounds", "true");
992
}
993
this.setAttribute("breakout-extend", "true");
994
995
// Enable the animation only after the first extend call to ensure it
996
// doesn't run when opening a new window.
997
if (!this.hasAttribute("breakout-extend-animate")) {
998
this.window.promiseDocumentFlushed(() => {
999
this.window.requestAnimationFrame(() => {
1000
this.setAttribute("breakout-extend-animate", "true");
1001
});
1002
});
1003
}
1004
}
1005
1006
endLayoutExtend() {
1007
if (
1008
!this.hasAttribute("breakout-extend") ||
1009
this.view.isOpen ||
1010
this.getAttribute("focused") == "true"
1011
) {
1012
return;
1013
}
1014
this.removeAttribute("breakout-extend");
1015
if (this._toolbar) {
1016
this._toolbar.removeAttribute("urlbar-exceeds-toolbar-bounds");
1017
}
1018
}
1019
1020
setPageProxyState(state) {
1021
this.setAttribute("pageproxystate", state);
1022
this._inputContainer.setAttribute("pageproxystate", state);
1023
this._identityBox.setAttribute("pageproxystate", state);
1024
}
1025
1026
// Private methods below.
1027
1028
async _updateLayoutBreakoutDimensions() {
1029
// When this method gets called a second time before the first call
1030
// finishes, we need to disregard the first one.
1031
let updateKey = {};
1032
this._layoutBreakoutUpdateKey = updateKey;
1033
1034
this.removeAttribute("breakout");
1035
this.textbox.parentNode.removeAttribute("breakout");
1036
1037
await this.window.promiseDocumentFlushed(() => {});
1038
await new Promise(resolve => {
1039
this.window.requestAnimationFrame(() => {
1040
if (this._layoutBreakoutUpdateKey != updateKey) {
1041
return;
1042
}
1043
1044
this.textbox.parentNode.style.setProperty(
1045
"--urlbar-container-height",
1046
px(getBoundsWithoutFlushing(this.textbox.parentNode).height)
1047
);
1048
this.textbox.style.setProperty(
1049
"--urlbar-height",
1050
px(getBoundsWithoutFlushing(this.textbox).height)
1051
);
1052
this.textbox.style.setProperty(
1053
"--urlbar-toolbar-height",
1054
px(getBoundsWithoutFlushing(this.textbox.closest("toolbar")).height)
1055
);
1056
1057
this.setAttribute("breakout", "true");
1058
this.textbox.parentNode.setAttribute("breakout", "true");
1059
1060
resolve();
1061
});
1062
});
1063
}
1064
1065
_setOpenViewOnFocus() {
1066
// FIXME: Not using UrlbarPrefs because its pref observer may run after
1067
// this call, so we'd get the previous openViewOnFocus value here. This
1068
// can be cleaned up after bug 1560013.
1069
this._openViewOnFocus = Services.prefs.getBoolPref(
1070
"browser.urlbar.openViewOnFocus"
1071
);
1072
this.dropmarker.hidden = this._openViewOnFocus;
1073
}
1074
1075
_setValue(val, allowTrim) {
1076
this._untrimmedValue = val;
1077
1078
let originalUrl = ReaderMode.getOriginalUrlObjectForDisplay(val);
1079
if (originalUrl) {
1080
val = originalUrl.displaySpec;
1081
}
1082
1083
val = allowTrim ? this._trimValue(val) : val;
1084
1085
this.valueIsTyped = false;
1086
this._resultForCurrentValue = null;
1087
this.inputField.value = val;
1088
this.formatValue();
1089
this.removeAttribute("actiontype");
1090
1091
// Dispatch ValueChange event for accessibility.
1092
let event = this.document.createEvent("Events");
1093
event.initEvent("ValueChange", true, true);
1094
this.inputField.dispatchEvent(event);
1095
1096
return val;
1097
}
1098
1099
_getValueFromResult(result) {
1100
switch (result.type) {
1101
case UrlbarUtils.RESULT_TYPE.KEYWORD:
1102
return result.payload.input;
1103
case UrlbarUtils.RESULT_TYPE.SEARCH:
1104
return (
1105
(result.payload.keyword ? result.payload.keyword + " " : "") +
1106
(result.payload.suggestion || result.payload.query)
1107
);
1108
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
1109
return result.payload.content;
1110
}
1111
1112
try {
1113
let uri = Services.io.newURI(result.payload.url);
1114
if (uri) {
1115
return this.window.losslessDecodeURI(uri);
1116
}
1117
} catch (ex) {}
1118
1119
return "";
1120
}
1121
1122
/**
1123
* Resets some state so that searches from the user's previous interaction
1124
* with the input don't interfere with searches from a new interaction.
1125
*/
1126
_resetSearchState() {
1127
this._lastSearchString = this.value;
1128
this._autofillPlaceholder = "";
1129
}
1130
1131
/**
1132
* Autofills the autofill placeholder string if appropriate, and determines
1133
* whether autofill should be allowed for the new search started by an input
1134
* event.
1135
*
1136
* @param {string} value
1137
* The new search string.
1138
* @returns {boolean}
1139
* Whether autofill should be allowed in the new search.
1140
*/
1141
_maybeAutofillOnInput(value) {
1142
let allowAutofill = this.selectionEnd == value.length;
1143
1144
// Determine whether we can autofill the placeholder. The placeholder is a
1145
// value that we autofill now, when the search starts and before we wait on
1146
// its first result, in order to prevent a flicker in the input caused by
1147
// the previous autofilled substring disappearing and reappearing when the
1148
// first result arrives. Of course we can only autofill the placeholder if
1149
// it starts with the new search string, and we shouldn't autofill anything
1150
// if the caret isn't at the end of the input.
1151
if (
1152
!allowAutofill ||
1153
this._autofillPlaceholder.length <= value.length ||
1154
!this._autofillPlaceholder
1155
.toLocaleLowerCase()
1156
.startsWith(value.toLocaleLowerCase())
1157
) {
1158
this._autofillPlaceholder = "";
1159
} else if (
1160
this._autofillPlaceholder &&
1161
this.selectionEnd == this.value.length &&
1162
this._enableAutofillPlaceholder
1163
) {
1164
let autofillValue =
1165
value + this._autofillPlaceholder.substring(value.length);
1166
this._autofillValue(autofillValue, value.length, autofillValue.length);
1167
}
1168
1169
return allowAutofill;
1170
}
1171
1172
_checkForRtlText(value) {
1173
let directionality = this.window.windowUtils.getDirectionFromText(value);
1174
if (directionality == this.window.windowUtils.DIRECTION_RTL) {
1175
this.setAttribute("rtltext", "true");
1176
return true;
1177
}
1178
this.removeAttribute("rtltext");
1179
return false;
1180
}
1181
1182
_updateTextOverflow() {
1183
if (!this._overflowing) {
1184
this.removeAttribute("textoverflow");
1185
return;
1186
}
1187
1188
let isRTL = this._checkForRtlText(this.value);
1189
1190
this.window.promiseDocumentFlushed(() => {
1191
// Check overflow again to ensure it didn't change in the meantime.
1192
let input = this.inputField;
1193
if (input && this._overflowing) {
1194
let side = isRTL ? "left" : "right";
1195
if (input.scrollLeft == input.scrollLeftMax) {
1196
side = isRTL == !input.scrollLeft ? "left" : "right";
1197
}
1198
1199
this.window.requestAnimationFrame(() => {
1200
// And check once again, since we might have stopped overflowing
1201
// since the promiseDocumentFlushed callback fired.
1202
if (this._overflowing) {
1203
this.setAttribute("textoverflow", side);
1204
}
1205
});
1206
}
1207
});
1208
}
1209
1210
_updateUrlTooltip() {
1211
if (this.focused || !this._overflowing) {
1212
this.inputField.removeAttribute("title");
1213
} else {
1214
this.inputField.setAttribute("title", this.untrimmedValue);
1215
}
1216
}
1217
1218
_getSelectedValueForClipboard() {
1219
let selection = this.editor.selection;
1220
const flags =
1221
Ci.nsIDocumentEncoder.OutputPreformatted |
1222
Ci.nsIDocumentEncoder.OutputRaw;
1223
let selectedVal = selection.toStringWithFormat("text/plain", flags, 0);
1224
1225
// Handle multiple-range selection as a string for simplicity.
1226
if (selection.rangeCount > 1) {
1227
return selectedVal;
1228
}
1229
1230
// If the selection doesn't start at the beginning or doesn't span the
1231
// full domain or the URL bar is modified or there is no text at all,
1232
// nothing else to do here.
1233
if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "") {
1234
return selectedVal;
1235
}
1236
1237
// The selection doesn't span the full domain if it doesn't contain a slash and is
1238
// followed by some character other than a slash.
1239
if (!selectedVal.includes("/")) {
1240
let remainder = this.value.replace(selectedVal, "");
1241
if (remainder != "" && remainder[0] != "/") {
1242
return selectedVal;
1243
}
1244
}
1245
1246
let uri;
1247
if (this.getAttribute("pageproxystate") == "valid") {
1248
uri = this.window.gBrowser.currentURI;
1249
} else {
1250
// The value could be:
1251
// 1. a trimmed url, set by selecting a result
1252
// 2. a search string set by selecting a result
1253
// 3. a url that was confirmed but didn't finish loading yet
1254
// If it's an url the untrimmedValue should resolve to a valid URI,
1255
// otherwise it's a search string that should be copied as-is.
1256
try {
1257
uri = Services.io.newURI(this._untrimmedValue);
1258
} catch (ex) {
1259
return selectedVal;
1260
}
1261
}
1262
uri = this.makeURIReadable(uri);
1263
1264
// If the entire URL is selected, just use the actual loaded URI,
1265
// unless we want a decoded URI, or it's a data: or javascript: URI,
1266
// since those are hard to read when encoded.
1267
if (
1268
this.value == selectedVal &&
1269
!uri.schemeIs("javascript") &&
1270
!uri.schemeIs("data") &&
1271
!UrlbarPrefs.get("decodeURLsOnCopy")
1272
) {
1273
return uri.displaySpec;
1274
}
1275
1276
// Just the beginning of the URL is selected, or we want a decoded
1277
// url. First check for a trimmed value.
1278
let spec = uri.displaySpec;
1279
let trimmedSpec = this._trimValue(spec);
1280
if (spec != trimmedSpec) {
1281
// Prepend the portion that _trimValue removed from the beginning.
1282
// This assumes _trimValue will only truncate the URL at
1283
// the beginning or end (or both).
1284
let trimmedSegments = spec.split(trimmedSpec);
1285
selectedVal = trimmedSegments[0] + selectedVal;
1286
}
1287
1288
return selectedVal;
1289
}
1290
1291
_toggleActionOverride(event) {
1292
// Ignore repeated KeyboardEvents.
1293
if (event.repeat) {
1294
return;
1295
}
1296
if (
1297
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
1298
event.keyCode == KeyEvent.DOM_VK_ALT ||
1299
event.keyCode ==
1300
(AppConstants.platform == "macosx"
1301
? KeyEvent.DOM_VK_META
1302
: KeyEvent.DOM_VK_CONTROL)
1303
) {
1304
if (event.type == "keydown") {
1305
this._actionOverrideKeyCount++;
1306
this.setAttribute("actionoverride", "true");
1307
this.view.panel.setAttribute("actionoverride", "true");
1308
} else if (
1309
this._actionOverrideKeyCount &&
1310
--this._actionOverrideKeyCount == 0
1311
) {
1312
this._clearActionOverride();
1313
}
1314
}
1315
}
1316
1317
_clearActionOverride() {
1318
this._actionOverrideKeyCount = 0;
1319
this.removeAttribute("actionoverride");
1320
this.view.panel.removeAttribute("actionoverride");
1321
}
1322
1323
/**
1324
* Get the url to load for the search query and records in telemetry that it
1325
* is being loaded.
1326
*
1327
* @param {nsISearchEngine} engine
1328
* The engine to generate the query for.
1329
* @param {Event} event
1330
* The event that triggered this query.
1331
* @param {object} searchActionDetails
1332
* The details associated with this search query.
1333
* @param {boolean} searchActionDetails.isSuggestion
1334
* True if this query was initiated from a suggestion from the search engine.
1335
* @param {alias} searchActionDetails.alias
1336
* True if this query was initiated via a search alias.
1337
*/
1338
_recordSearch(engine, event, searchActionDetails = {}) {
1339
const isOneOff = this.view.oneOffSearchButtons.maybeRecordTelemetry(event);
1340
// Infer the type of the event which triggered the search.
1341
let eventType = "unknown";
1342
if (event instanceof KeyboardEvent) {
1343
eventType = "key";
1344
} else if (event instanceof MouseEvent) {
1345
eventType = "mouse";
1346
}
1347
// Augment the search action details object.
1348
let details = searchActionDetails;
1349
details.isOneOff = isOneOff;
1350
details.type = eventType;
1351
1352
this.window.BrowserSearch.recordSearchInTelemetry(
1353
engine,
1354
"urlbar",
1355
details
1356
);
1357
}
1358
1359
/**
1360
* Shortens the given value, usually by removing http:// and trailing slashes,
1361
* such that calling nsIURIFixup::createFixupURI with the result will produce
1362
* the same URI.
1363
*
1364
* @param {string} val
1365
* The string to be trimmed if it appears to be URI
1366
* @returns {string}
1367
* The trimmed string
1368
*/
1369
_trimValue(val) {
1370
return UrlbarPrefs.get("trimURLs") ? BrowserUtils.trimURL(val) : val;
1371
}
1372
1373
/**
1374
* If appropriate, this prefixes a search string with 'www.' and suffixes it
1375
* with browser.fixup.alternate.suffix prior to navigating.
1376
*
1377
* @param {Event} event
1378
* The event that triggered this query.
1379
* @param {string} value
1380
* The search string that should be canonized.
1381
* @returns {string}
1382
* Returns the canonized URL if available and null otherwise.
1383
*/
1384
_maybeCanonizeURL(event, value) {
1385
// Only add the suffix when the URL bar value isn't already "URL-like",
1386
// and only if we get a keyboard event, to match user expectations.
1387
if (
1388
!(event instanceof KeyboardEvent) ||
1389
!event.ctrlKey ||
1390
!UrlbarPrefs.get("ctrlCanonizesURLs") ||
1391
!/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value)
1392
) {
1393
return null;
1394
}
1395
1396
let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
1397
if (!suffix.endsWith("/")) {
1398
suffix += "/";
1399
}
1400
1401
// trim leading/trailing spaces (bug 233205)
1402
value = value.trim();
1403
1404
// Tack www. and suffix on. If user has appended directories, insert
1405
// suffix before them (bug 279035). Be careful not to get two slashes.
1406
let firstSlash = value.indexOf("/");
1407
if (firstSlash >= 0) {
1408
value =
1409
value.substring(0, firstSlash) +
1410
suffix +
1411
value.substring(firstSlash + 1);
1412
} else {
1413
value = value + suffix;
1414
}
1415
value = "http://www." + value;
1416
1417
this.value = value;
1418
return value;
1419
}
1420
1421
/**
1422
* Autofills a value into the input. The value will be autofilled regardless
1423
* of the input's current value.
1424
*
1425
* @param {string} value
1426
* The value to autofill.
1427
* @param {integer} selectionStart
1428
* The new selectionStart.
1429
* @param {integer} selectionEnd
1430
* The new selectionEnd.
1431
*/
1432
_autofillValue(value, selectionStart, selectionEnd) {
1433
// The autofilled value may be a URL that includes a scheme at the
1434
// beginning. Do not allow it to be trimmed.
1435
this._setValue(value, false);
1436
this.selectionStart = selectionStart;
1437
this.selectionEnd = selectionEnd;
1438
this._autofillPlaceholder = value;
1439
}
1440
1441
/**
1442
* Loads the url in the appropriate place.
1443
*
1444
* @param {string} url
1445
* The URL to open.
1446
* @param {string} openUILinkWhere
1447
* Where we expect the result to be opened.
1448
* @param {object} params
1449
* The parameters related to how and where the result will be opened.
1450
* Further supported paramters are listed in utilityOverlay.js#openUILinkIn.
1451
* @param {object} params.triggeringPrincipal
1452
* The principal that the action was triggered from.
1453
* @param {nsIInputStream} [params.postData]
1454
* The POST data associated with a search submission.
1455
* @param {boolean} [params.allowInheritPrincipal]
1456
* Whether the principal can be inherited.
1457
* @param {object} [resultDetails]
1458
* Details of the selected result, if any.
1459
* @param {UrlbarUtils.RESULT_TYPE} [result.type]
1460
* Details of the result type, if any.
1461
* @param {UrlbarUtils.RESULT_SOURCE} [result.source]
1462
* Details of the result source, if any.
1463
* @param {object} browser [optional] the browser to use for the load.
1464
*/
1465
_loadURL(
1466
url,
1467
openUILinkWhere,
1468
params,
1469
resultDetails = null,
1470
browser = this.window.gBrowser.selectedBrowser
1471
) {
1472
// No point in setting these because we'll handleRevert() a few rows below.
1473
if (openUILinkWhere == "current") {
1474
this.value = url;
1475
browser.userTypedValue = url;
1476
}
1477
1478
// No point in setting this if we are loading in a new window.
1479
if (
1480
openUILinkWhere != "window" &&
1481
this.window.gInitialPages.includes(url)
1482
) {
1483
browser.initialPageLoadedFromUserAction = url;
1484
}
1485
1486
try {
1487
UrlbarUtils.addToUrlbarHistory(url, this.window);
1488
} catch (ex) {
1489
// Things may go wrong when adding url to session history,
1490
// but don't let that interfere with the loading of the url.
1491
Cu.reportError(ex);
1492
}
1493
1494
// TODO: When bug 1498553 is resolved, we should be able to
1495
// remove the !triggeringPrincipal condition here.
1496
if (
1497
!params.triggeringPrincipal ||
1498
params.triggeringPrincipal.isSystemPrincipal
1499
) {
1500
// Reset DOS mitigations for the basic auth prompt.
1501
delete browser.authPromptAbuseCounter;
1502
1503
// Reset temporary permissions on the current tab if the user reloads
1504
// the tab via the urlbar.
1505
if (
1506
openUILinkWhere == "current" &&
1507
browser.currentURI &&
1508
url === browser.currentURI.spec
1509
) {
1510
this.window.SitePermissions.clearTemporaryPermissions(browser);
1511
}
1512
}
1513
1514
params.allowThirdPartyFixup = true;
1515
1516
if (openUILinkWhere == "current") {
1517
params.targetBrowser = browser;
1518
params.indicateErrorPageLoad = true;
1519
params.allowPinnedTabHostChange = true;
1520
params.allowPopups = url.startsWith("javascript:");
1521
} else {
1522
params.initiatingDoc = this.window.document;
1523
}
1524
1525
// Focus the content area before triggering loads, since if the load
1526
// occurs in a new tab, we want focus to be restored to the content
1527
// area when the current tab is re-selected.
1528
browser.focus();
1529
1530
if (openUILinkWhere != "current") {
1531
this.handleRevert();
1532
}
1533
1534
// Notify about the start of navigation.
1535
this._notifyStartNavigation(resultDetails);
1536
1537
try {
1538
this.window.openTrustedLinkIn(url, openUILinkWhere, params);
1539
} catch (ex) {
1540
// This load can throw an exception in certain cases, which means
1541
// we'll want to replace the URL with the loaded URL:
1542
if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
1543
this.handleRevert();
1544
}
1545
}
1546
1547
// Make sure the domain name stays visible for spoof protection and usability.
1548
this.selectionStart = this.selectionEnd = 0;
1549
1550
this.view.close();
1551
}
1552
1553
/**
1554
* Determines where a URL/page should be opened.
1555
*
1556
* @param {Event} event the event triggering the opening.
1557
* @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
1558
*/
1559
_whereToOpen(event) {
1560
let isMouseEvent = event instanceof MouseEvent;
1561
let reuseEmpty = !isMouseEvent;
1562
let where = undefined;
1563
if (
1564
!isMouseEvent &&
1565
event &&
1566
(event.altKey || event.getModifierState("AltGraph"))
1567
) {
1568
// We support using 'alt' to open in a tab, because ctrl/shift
1569
// might be used for canonizing URLs:
1570
where = event.shiftKey ? "tabshifted" : "tab";
1571
} else if (
1572
!isMouseEvent &&
1573
event &&
1574
event.ctrlKey &&
1575
UrlbarPrefs.get("ctrlCanonizesURLs")
1576
) {
1577
// If we're allowing canonization, and this is a key event with ctrl
1578
// pressed, open in current tab to allow ctrl-enter to canonize URL.
1579
where = "current";
1580
} else {
1581
where = this.window.whereToOpenLink(event, false, false);
1582
}
1583
if (UrlbarPrefs.get("openintab")) {
1584
if (where == "current") {
1585
where = "tab";
1586
} else if (where == "tab") {
1587
where = "current";
1588
}
1589
reuseEmpty = true;
1590
}
1591
if (
1592
where == "tab" &&
1593
reuseEmpty &&
1594
this.window.gBrowser.selectedTab.isEmpty
1595
) {
1596
where = "current";
1597
}
1598
return where;
1599
}
1600
1601
_initCopyCutController() {
1602
this._copyCutController = new CopyCutController(this);
1603
this.inputField.controllers.insertControllerAt(0, this._copyCutController);
1604
}
1605
1606
_initPasteAndGo() {
1607
let inputBox = this.querySelector("moz-input-box");
1608
let contextMenu = inputBox.menupopup;
1609
let insertLocation = contextMenu.firstElementChild;
1610
while (
1611
insertLocation.nextElementSibling &&
1612
insertLocation.getAttribute("cmd") != "cmd_paste"
1613
) {
1614
insertLocation = insertLocation.nextElementSibling;
1615
}
1616
if (!insertLocation) {
1617
return;
1618
}
1619
1620
let pasteAndGo = this.document.createXULElement("menuitem");
1621
let label = Services.strings
1623
.GetStringFromName("pasteAndGo.label");
1624
pasteAndGo.setAttribute("label", label);
1625
pasteAndGo.setAttribute("anonid", "paste-and-go");
1626
pasteAndGo.addEventListener("command", () => {
1627
this._suppressStartQuery = true;
1628
1629
this.select();
1630
this.window.goDoCommand("cmd_paste");
1631
this.handleCommand();
1632
1633
this._suppressStartQuery = false;
1634
});
1635
1636
contextMenu.addEventListener("popupshowing", () => {
1637
// Close the results pane when the input field contextual menu is open,
1638
// because paste and go doesn't want a result selection.
1639
this.view.close();
1640
1641
let controller = this.document.commandDispatcher.getControllerForCommand(
1642
"cmd_paste"
1643
);
1644
let enabled = controller.isCommandEnabled("cmd_paste");
1645
if (enabled) {
1646
pasteAndGo.removeAttribute("disabled");
1647
} else {
1648
pasteAndGo.setAttribute("disabled", "true");
1649
}
1650
});
1651
1652
insertLocation.insertAdjacentElement("afterend", pasteAndGo);
1653
}
1654
1655
/**
1656
* This notifies observers that the user has entered or selected something in
1657
* the URL bar which will cause navigation.
1658
*
1659
* We use the observer service, so that we don't need to load extra facilities
1660
* if they aren't being used, e.g. WebNavigation.
1661
*
1662
* @param {UrlbarResult} result
1663
* Details of the result that was selected, if any.
1664
*/
1665
_notifyStartNavigation(result) {
1666
Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation");
1667
}
1668
1669
/**
1670
* Determines if we should select all the text in the Urlbar based on the
1671
* clickSelectsAll pref, Urlbar state, and whether the selection is empty.
1672
* @param {boolean} [ignoreClickSelectsAllPref]
1673
* If true, the browser.urlbar.clickSelectsAll pref will be ignored.
1674
*/
1675
_maybeSelectAll(ignoreClickSelectsAllPref = false) {
1676
if (
1677
!this._preventClickSelectsAll &&
1678
(ignoreClickSelectsAllPref || UrlbarPrefs.get("clickSelectsAll")) &&
1679
this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING &&
1680
this.document.activeElement == this.inputField &&
1681
this.inputField.selectionStart == this.inputField.selectionEnd
1682
) {
1683
this.editor.selectAll();
1684
}
1685
}
1686
1687
// Event handlers below.
1688
1689
_on_command(event) {
1690
// Something is executing a command, likely causing a focus change. This
1691
// should not be recorded as an abandonment.
1692
this.controller.engagementEvent.discard();
1693
}
1694
1695
_on_blur(event) {
1696
// We cannot count every blur events after a missed engagement as abandoment
1697
// because the user may have clicked on some view element that executes
1698
// a command causing a focus change. For example opening preferences from
1699
// the oneoff settings button.
1700
// For now we detect that case by discarding the event on command, but we
1701
// may want to figure out a more robust way to detect abandonment.
1702
this.controller.engagementEvent.record(event, {
1703
numChars: this._lastSearchString.length,
1704
});
1705
1706
this.removeAttribute("focused");
1707
this.endLayoutExtend();
1708
1709
if (this._autofillPlaceholder && this.window.gBrowser.userTypedValue) {
1710
// Restore value to the last typed one, removing any autofilled portion.
1711
this.value = this.window.gBrowser.userTypedValue;
1712
}
1713
1714
this.formatValue();
1715
this._resetSearchState();
1716
1717
// In certain cases, like holding an override key and confirming an entry,
1718
// we don't key a keyup event for the override key, thus we make this
1719
// additional cleanup on blur.
1720
this._clearActionOverride();
1721
1722
// The extension input sessions depends more on blur than on the fact we
1723
// actually cancel a running query, so we do it here.
1724
if (ExtensionSearchHandler.hasActiveInputSession()) {
1725
ExtensionSearchHandler.handleInputCancelled();
1726
}
1727
1728
// Respect the autohide preference for easier inspecting/debugging via
1729
// the browser toolbox.
1730
if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
1731
this.view.close();
1732
}
1733
1734
if (this._revertOnBlurValue == this.value) {
1735
this.handleRevert();
1736
}
1737
this._revertOnBlurValue = null;
1738
1739
// We may have hidden popup notifications, show them again if necessary.
1740
if (this.getAttribute("pageproxystate") != "valid") {
1741
this.window.UpdatePopupNotificationsVisibility();
1742
}
1743
1744
Services.obs.notifyObservers(null, "urlbar-blur");
1745
}
1746
1747
_on_click(event) {
1748
if (
1749
event.target == this.inputField ||
1750
event.target == this._inputContainer ||
1751
event.target.id == SEARCH_BUTTON_ID
1752
) {
1753
this.startLayoutExtend();
1754
this._maybeSelectAll();
1755
}
1756
}
1757
1758
_on_contextmenu(event) {
1759
// Context menu opened via keyboard shortcut.
1760
if (!event.button) {
1761
return;
1762
}
1763
1764
// If the user right clicks, we select all regardless of the value of
1765
// the browser.urlbar.clickSelectsAll pref.
1766
this._maybeSelectAll(/* ignoreClickSelectsAllPref */ event.button == 2);
1767
}
1768
1769
_on_focus(event) {
1770
if (!this._hideFocus) {
1771
this.setAttribute("focused", "true");
1772
}
1773
1774
// We handle mouse-based expansion events separately in _on_click.
1775
if (this._focusedViaMousedown) {
1776
this._focusedViaMousedown = false;
1777
this.view.maybeReopen();
1778
} else {
1779
this.startLayoutExtend();
1780
if (this.inputField.hasAttribute("refocused-by-panel")) {
1781
this._maybeSelectAll(true);
1782
}
1783
}
1784
1785
this._updateUrlTooltip();
1786
this.formatValue();
1787
1788
// Hide popup notifications, to reduce visual noise.
1789
if (this.getAttribute("pageproxystate") != "valid") {
1790
this.window.UpdatePopupNotificationsVisibility();
1791
}
1792
1793
Services.obs.notifyObservers(null, "urlbar-focus");
1794
}
1795
1796
_on_mouseover(event) {
1797
this._updateUrlTooltip();
1798
}
1799
1800
_on_mousedown(event) {
1801
switch (event.currentTarget) {
1802
case this.textbox:
1803
this._mousedownOnUrlbarDescendant = true;
1804
1805
if (
1806
event.target != this.inputField &&
1807
event.target != this._inputContainer &&
1808
event.target.id != SEARCH_BUTTON_ID
1809
) {
1810
break;
1811
}
1812
1813
this._focusedViaMousedown = !this.focused;
1814
this._preventClickSelectsAll = this.focused;
1815
1816
if (event.target != this.inputField) {
1817
this.focus();
1818
}
1819
1820
// The rest of this case only cares about left clicks.
1821
if (event.button != 0) {
1822
break;
1823
}
1824
1825
// Clear any previous selection unless we are focused, to ensure it
1826
// doesn't affect drag selection.
1827
if (this._focusedViaMousedown) {
1828
this.selectionStart = this.selectionEnd = 0;
1829
}
1830
1831
if (event.detail == 2 && UrlbarPrefs.get("doubleClickSelectsAll")) {
1832
this.editor.selectAll();
1833
event.preventDefault();
1834
} else if (event.target.id == SEARCH_BUTTON_ID) {
1835
this._preventClickSelectsAll = true;
1836
this.search(UrlbarTokenizer.RESTRICT.SEARCH);
1837
} else if (this.openViewOnFocus && !this.view.isOpen) {
1838
this.startQuery({
1839
allowAutofill: false,
1840
event,
1841
});
1842
}
1843
break;
1844
case this.dropmarker:
1845
if (event.button != 0) {
1846
break;
1847
}
1848
1849
if (this.view.isOpen) {
1850
this.view.close();
1851
} else {
1852
this.focus();
1853
this.startQuery({
1854
allowAutofill: false,
1855
event,
1856
});
1857
this._maybeSelectAll();
1858
}
1859
break;
1860
case this.window:
1861
if (this._mousedownOnUrlbarDescendant) {
1862
this._mousedownOnUrlbarDescendant = false;
1863
break;
1864
}
1865
// Don't close the view when clicking on a tab; we may want to keep the
1866
// view open on tab switch, and the TabSelect event arrived earlier.
1867
if (event.target.closest("tab")) {
1868
break;
1869
}
1870
// Close the view when clicking on toolbars and other UI pieces that
1871
// might not automatically remove focus from the input.
1872
// Respect the autohide preference for easier inspecting/debugging via
1873
// the browser toolbox.
1874
if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
1875
this.view.close();
1876
}
1877
break;
1878
}
1879
}
1880
1881
_on_input(event) {
1882
let value = this.value;
1883
this.valueIsTyped = true;
1884
this._untrimmedValue = value;
1885
this.window.gBrowser.userTypedValue = value;
1886
// Unset userSelectionBehavior because the user is modifying the search
1887
// string, thus there's no valid selection. This is also used by the view
1888
// to set "aria-activedescendant", thus it should never get stale.
1889
this.controller.userSelectionBehavior = "none";
1890
1891
let compositionState = this._compositionState;
1892
let compositionClosedPopup = this._compositionClosedPopup;
1893
1894
// Clear composition values if we're no more composing.
1895
if (this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING) {
1896
this._compositionState = UrlbarUtils.COMPOSITION.NONE;
1897
this._compositionClosedPopup = false;
1898
}
1899
1900
if (value) {
1901
this.setAttribute("usertyping", "true");
1902
this._checkForRtlText(value);
1903
} else {
1904
this.removeAttribute("usertyping");
1905
}
1906
this.removeAttribute("actiontype");
1907
1908
if (!this.view.isOpen || !value) {
1909
this.view.clear();
1910
}
1911
if (this.view.isOpen && !value) {
1912
this.view.close();
1913
return;
1914
}
1915
1916
this.view.removeAccessibleFocus();
1917
1918
// During composition with an IME, the following events happen in order:
1919
// 1. a compositionstart event
1920
// 2. some input events
1921
// 3. a compositionend event
1922
// 4. an input event
1923
1924
// We should do nothing during composition or if composition was canceled
1925
// and we didn't close the popup on composition start.
1926
if (
1927
compositionState == UrlbarUtils.COMPOSITION.COMPOSING ||
1928
(compositionState == UrlbarUtils.COMPOSITION.CANCELED &&
1929
!compositionClosedPopup)
1930
) {
1931
return;
1932
}
1933
1934
// Autofill only when text is inserted (i.e., event.data is not empty) and
1935
// it's not due to pasting.
1936
let allowAutofill =
1937
!!event.data &&
1938
!UrlbarUtils.isPasteEvent(event) &&
1939
this._maybeAutofillOnInput(value);
1940
1941
this.startQuery({
1942
searchString: value,
1943
allowAutofill,
1944
resetSearchState: false,
1945
event,
1946
});
1947
}
1948
1949
_on_select(event) {
1950
// On certain user input, AutoCopyListener::OnSelectionChange() updates
1951
// the primary selection with user-selected text (when supported).
1952
// Selection::NotifySelectionListeners() then dispatches a "select" event
1953
// under similar conditions via TextInputListener::OnSelectionChange().
1954
// This event is received here in order to replace the primary selection
1955
// from the editor with text having the adjustments of
1956
// _getSelectedValueForClipboard(), such as adding the scheme for the url.
1957
//
1958
// Other "select" events are also received, however, and must be excluded.
1959
if (
1960
// _suppressPrimaryAdjustment is set during select(). Don't update
1961
// the primary selection because that is not the intent of user input,
1962
// which may be new tab or urlbar focus.
1963
this._suppressPrimaryAdjustment ||
1964
// The check on isHandlingUserInput filters out async "select" events
1965
// from setSelectionRange(), which occur when autofill text is selected.
1966
!this.window.windowUtils.isHandlingUserInput ||
1967
!Services.clipboard.supportsSelectionClipboard()
1968
) {
1969
return;
1970
}
1971
1972
let val = this._getSelectedValueForClipboard();
1973
if (!val) {
1974
return;
1975
}
1976
1977
ClipboardHelper.copyStringToClipboard(
1978
val,
1979
Services.clipboard.kSelectionClipboard
1980
);
1981
}
1982
1983
_on_overflow(event) {
1984
const targetIsPlaceholder = !event.originalTarget.classList.contains(
1985
"anonymous-div"
1986
);
1987
// We only care about the non-placeholder text.
1988
// This shouldn't be needed, see bug 1487036.
1989
if (targetIsPlaceholder) {
1990
return;
1991
}
1992
this._overflowing = true;
1993
this._updateTextOverflow();
1994
}
1995
1996
_on_underflow(event) {
1997
const targetIsPlaceholder = !event.originalTarget.classList.contains(
1998
"anonymous-div"
1999
);
2000
// We only care about the non-placeholder text.
2001
// This shouldn't be needed, see bug 1487036.
2002
if (targetIsPlaceholder) {
2003
return;
2004
}
2005
this._overflowing = false;
2006
2007
this._updateTextOverflow();
2008
2009
this._updateUrlTooltip();
2010
}
2011
2012
_on_paste(event) {
2013
let originalPasteData = event.clipboardData.getData("text/plain");
2014
if (!originalPasteData) {
2015
return;
2016
}
2017
2018
let oldValue = this.inputField.value;
2019
let oldStart = oldValue.substring(0, this.selectionStart);
2020
// If there is already non-whitespace content in the URL bar
2021
// preceding the pasted content, it's not necessary to check
2022
// protocols used by the pasted content:
2023
if (oldStart.trim()) {
2024
return;
2025
}
2026
let oldEnd = oldValue.substring(this.selectionEnd);
2027
2028
let pasteData = UrlbarUtils.stripUnsafeProtocolOnPaste(originalPasteData);
2029
if (originalPasteData != pasteData) {
2030
// Unfortunately we're not allowed to set the bits being pasted
2031
// so cancel this event:
2032
event.preventDefault();
2033
event.stopImmediatePropagation();
2034
2035
this.inputField.value = oldStart + pasteData + oldEnd;
2036
// Fix up cursor/selection:
2037
let newCursorPos = oldStart.length + pasteData.length;
2038
this.selectionStart = newCursorPos;
2039
this.selectionEnd = newCursorPos;
2040
}
2041
}
2042
2043
_on_scrollend(event) {
2044
this._updateTextOverflow();
2045
}
2046
2047
_on_TabSelect(event) {
2048
this._resetSearchState();
2049
}
2050
2051
_on_keydown(event) {
2052
// Due to event deferring, it's possible preventDefault() won't be invoked
2053
// soon enough to actually prevent some of the default behaviors, thus we
2054
// have to handle the event "twice". This first immediate call passes false
2055
// as second argument so that handleKeyNavigation will only simulate the
2056
// event handling, without actually executing actions.
2057
// TODO (Bug 1541806): improve this handling, maybe by delaying actions
2058
// instead of events.
2059
if (this.eventBufferer.shouldDeferEvent(event)) {
2060
this.controller.handleKeyNavigation(event, false);
2061
}
2062
this._toggleActionOverride(event);
2063
this.eventBufferer.maybeDeferEvent(event, () => {
2064
this.controller.handleKeyNavigation(event);
2065
});
2066
}
2067
2068
_on_keyup(event) {
2069
this._toggleActionOverride(event);
2070
}
2071
2072
_on_compositionstart(event) {
2073
if (this._compositionState == UrlbarUtils.COMPOSITION.COMPOSING) {
2074
throw new Error("Trying to start a nested composition?");
2075
}
2076
this._compositionState = UrlbarUtils.COMPOSITION.COMPOSING;
2077
2078
// Close the view. This will also stop searching.
2079
if (this.view.isOpen) {
2080
this._compositionClosedPopup = true;
2081
this.view.close();
2082
} else {
2083
this._compositionClosedPopup = false;
2084
}
2085
}
2086
2087
_on_compositionend(event) {
2088
if (this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING) {
2089
throw new Error("Trying to stop a non existing composition?");
2090
}
2091
2092
// We can't yet retrieve the committed value from the editor, since it isn't
2093
// completely committed yet. We'll handle it at the next input event.
2094
this._compositionState = event.data
2095
? UrlbarUtils.COMPOSITION.COMMIT
2096
: UrlbarUtils.COMPOSITION.CANCELED;
2097
}