in mail/components/compose/content/addressingWidgetOverlay.js [509:794]
function addressInputOnBeforeHandleKeyDown(event) {
const input = event.target;
switch (event.key) {
case "a": {
// Break if there's text in the input, if not Ctrl/Cmd+A, or for other
// modifiers, to not hijack our own (Ctrl/Cmd+Shift+A) or OS shortcuts.
if (
input.value ||
!(AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) ||
event.shiftKey ||
event.altKey
) {
break;
}
// Ctrl/Cmd+A on empty input: Select all pills of the current row.
// Prevent a pill keypress event when the focus moves on it.
event.preventDefault();
const lastPill = input
.closest(".address-container")
.querySelector("mail-address-pill:last-of-type");
const mailRecipientsArea = input.closest("mail-recipients-area");
if (lastPill) {
// Select all pills of current address row.
mailRecipientsArea.selectSiblingPills(lastPill);
lastPill.focus();
break;
}
// No pills in the current address row, select all pills in all rows.
const lastPillGlobal = mailRecipientsArea.querySelector(
"mail-address-pill:last-of-type"
);
if (lastPillGlobal) {
mailRecipientsArea.selectAllPills();
lastPillGlobal.focus();
}
break;
}
case " ":
case ",": {
const selection = input.value.substring(
input.selectionStart,
input.selectionEnd
);
// If keydown would normally replace all of the current trimmed input,
// including if the current input is empty, then suppress the key and
// clear the input instead.
if (selection.includes(input.value.trim())) {
event.preventDefault();
input.value = "";
break;
}
// Otherwise, comma may trigger pill creation.
if (event.key !== ",") {
break;
}
let beforeComma;
let afterComma;
if (input.selectionEnd == input.selectionStart) {
// If there is no selected text, we will try to create a pill for the
// text prior to the typed comma.
// NOTE: This also captures auto complete suggestions that are not
// inline. E.g. suggestion popup is shown and the user selects one with
// the arrow keys.
beforeComma = input.value.substring(0, input.selectionEnd);
afterComma = input.value.substring(input.selectionEnd);
// Only create a pill for valid addresses.
if (!isValidAddress(beforeComma)) {
break;
}
} else if (
// There is an auto complete suggestion ...
input.controller.searchStatus ==
Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH &&
input.controller.matchCount &&
// that is also shown inline (the end of the input is selected).
input.selectionEnd == input.value.length
// NOTE: This should exclude cases where no suggestion is selected (user
// presses "DownArrow" then "UpArrow" when the suggestion pops up), or
// if the suggestions were cancelled with "Esc", or the inline
// suggestion was cleared with "Backspace".
) {
if (input.value[input.selectionStart] == ",") {
// Don't create the pill in the special case where the auto-complete
// suggestion starts with a comma.
break;
}
// Complete the suggestion as a pill.
beforeComma = input.value;
afterComma = "";
} else {
// If any other part of the text is selected, we treat it as normal.
break;
}
event.preventDefault();
input.value = beforeComma;
input.handleEnter(event);
// Keep any left over text in the input.
input.value = afterComma;
// Keep the cursor at the same position.
input.selectionStart = 0;
input.selectionEnd = 0;
break;
}
case "Home":
case "ArrowLeft":
case "Backspace": {
if (
event.key == "Backspace" &&
event.repeat &&
gPreventRowDeletionKeysRepeat
) {
// Prevent repeated backspace keydown event if the flag is set.
event.preventDefault();
break;
}
// Enable repeated deletion if Home or ArrowLeft were pressed, or if it is
// a non-repeated Backspace keydown event, or if the flag is already false.
gPreventRowDeletionKeysRepeat = false;
if (
input.value.trim() ||
input.selectionStart + input.selectionEnd ||
event.altKey
) {
// Break and allow the key's default behavior if the row has content,
// or the cursor is not at position 0, or the Alt modifier is pressed.
break;
}
// Navigate into pills if there are any, and if the input is empty or
// whitespace-only, and the cursor is at position 0, and the Alt key was
// not used (prevent undo via Alt+Backspace from deleting pills).
// We'll sanitize whitespace on blur.
// Prevent a pill keypress event when the focus moves on it, or prevent
// deletion in previous row after removing current row via long keydown.
event.preventDefault();
const targetPill = input
.closest(".address-container")
.querySelector(
"mail-address-pill" + (event.key == "Home" ? "" : ":last-of-type")
);
if (targetPill) {
if (event.repeat) {
// Prevent navigating into pills for repeated keydown from the middle
// of whitespace.
break;
}
input
.closest("mail-recipients-area")
.checkKeyboardSelected(event, targetPill);
// Prevent removing the current row after deleting the last pill with
// repeated deletion keydown.
gPreventRowDeletionKeysRepeat = true;
break;
}
// No pill found, so the address row is empty except whitespace.
// Check for long Backspace keyboard shortcut to remove the row.
if (
event.key != "Backspace" ||
!event.repeat ||
input
.closest(".address-row")
.querySelector(".remove-field-button[hidden]")
) {
break;
}
// Set flag to prevent further unwarranted deletion in the previous row,
// which will receive focus while the key is still down. We have already
// prevented the event above.
gPreventRowDeletionKeysRepeat = true;
// Hide the address row if it is empty except whitespace, repeated
// Backspace keydown event occurred, and it has an [x] button for removal.
hideAddressRowFromWithin(input, "previous");
break;
}
case "Delete": {
if (event.repeat && gPreventRowDeletionKeysRepeat) {
// Prevent repeated Delete keydown event if the flag is set.
event.preventDefault();
break;
}
// Enable repeated deletion in case of a non-repeated Delete keydown event,
// or if the flag is already false.
gPreventRowDeletionKeysRepeat = false;
if (
!event.repeat ||
input.value.trim() ||
input.selectionStart + input.selectionEnd ||
input
.closest(".address-container")
.querySelector("mail-address-pill") ||
input
.closest(".address-row")
.querySelector(".remove-field-button[hidden]")
) {
// Break and allow the key's default behaviour if the address row has
// content, or the cursor is not at position 0, or the row is not
// removable.
break;
}
// Prevent the event and set flag to prevent further unwarranted deletion
// in the next row, which will receive focus while the key is still down.
event.preventDefault();
gPreventRowDeletionKeysRepeat = true;
// Hide the address row if it is empty except whitespace, repeated Delete
// keydown event occurred, cursor is at position 0, and it has an
// [x] button for removal.
hideAddressRowFromWithin(input, "next");
break;
}
case "Enter": {
// Break if unrelated modifier keys are used. The toolkit hack for Mac
// will consume metaKey, and we'll exclude shiftKey after that.
if (event.ctrlKey || event.altKey) {
break;
}
// MacOS-only variation necessary to send messages via Cmd+[Shift]+Enter
// since autocomplete input fields prevent that by default (bug 1682147).
if (event.metaKey) {
// Cmd+[Shift]+Enter: Send message [later].
const sendCmd = event.shiftKey ? "cmd_sendLater" : "cmd_sendWithCheck";
goDoCommand(sendCmd);
break;
}
// Break if there's text in the address input, or if Shift modifier is
// used, to prevent hijacking shortcuts like Ctrl+Shift+Enter.
if (input.value.trim() || event.shiftKey) {
break;
}
// Enter on empty input: Focus the next available address row or subject.
// Prevent Enter from firing again on the element we move the focus to.
event.preventDefault();
focusNextAddressRow(input);
break;
}
case "Tab": {
// Return if the Alt or Cmd modifiers were pressed, meaning the user is
// switching between windows and not tabbing out of the address input.
if (event.altKey || event.metaKey) {
break;
}
// Trigger the autocomplete controller only if we have a value,
// to prevent interfering with the natural change of focus on Tab.
if (input.value.trim()) {
// Prevent Tab from firing again on address input after pill creation.
event.preventDefault();
// Use the setTimeout only if the input field implements a forced
// autocomplete and we don't have any match as we might need to wait for
// the autocomplete suggestions to show up.
if (input.forceComplete && input.mController.matchCount == 0) {
// Prevent fast user input to become an error pill before
// autocompletion kicks in with its default timeout.
setTimeout(() => {
input.handleEnter(event);
}, input.timeout);
} else {
input.handleEnter(event);
}
}
// Handle Shift+Tab, but not Ctrl+Shift+Tab, which is handled by
// moveFocusToNeighbouringAreas.
if (event.shiftKey && !event.ctrlKey) {
event.preventDefault();
input.closest("mail-recipients-area").moveFocusToPreviousElement(input);
}
break;
}
}
}