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