releases-comm-central/mail/base/content/threadPane.js

404 строки
12 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* import-globals-from folderDisplay.js */
/* import-globals-from SearchDialog.js */
/* globals validateFileName */ // From utilityOverlay.js
/* globals messageFlavorDataProvider */ // From messenger.js
/* exported ThreadPaneKeyDown ThreadPaneOnDragStart UpdateSortIndicators */
ChromeUtils.defineESModuleGetters(this, {
ThreadPaneColumns: "chrome://messenger/content/ThreadPaneColumns.mjs",
TreeSelection: "chrome://messenger/content/TreeSelection.mjs",
});
/**
* When right-clicks happen, we do not want to corrupt the underlying
* selection. The right-click is a transient selection. So, unless the
* user is right-clicking on the current selection, we create a new
* selection object (thanks to TreeSelection) and set that as the
* current/transient selection.
*
* @param aSingleSelect Should the selection we create be a single selection?
* This is relevant if the row being clicked on is already part of the
* selection. If it is part of the selection and !aSingleSelect, then we
* leave the selection as is. If it is part of the selection and
* aSingleSelect then we create a transient single-row selection.
*/
function ChangeSelectionWithoutContentLoad(event, tree, aSingleSelect) {
var treeSelection = tree.view.selection;
var row = tree.getRowAt(event.clientX, event.clientY);
// Only do something if:
// - the row is valid
// - it's not already selected (or we want a single selection)
if (row >= 0 && (aSingleSelect || !treeSelection.isSelected(row))) {
// Check if the row is exactly the existing selection. In that case
// there is no need to create a bogus selection.
if (treeSelection.count == 1) {
const minObj = {};
treeSelection.getRangeAt(0, minObj, {});
if (minObj.value == row) {
event.stopPropagation();
return;
}
}
const transientSelection = new TreeSelection(tree);
transientSelection.logAdjustSelectionForReplay();
var saveCurrentIndex = treeSelection.currentIndex;
// tell it to log calls to adjustSelection
// attach it to the view
tree.view.selection = transientSelection;
// Don't generate any selection events! (we never set this to false, because
// that would generate an event, and we never need one of those from this
// selection object.
transientSelection.selectEventsSuppressed = true;
transientSelection.select(row);
transientSelection.currentIndex = saveCurrentIndex;
tree.ensureRowIsVisible(row);
}
event.stopPropagation();
}
function ThreadPaneOnDragStart(aEvent) {
if (aEvent.target.localName != "treechildren") {
return;
}
const messageUris = gFolderDisplay.selectedMessageUris;
if (!messageUris) {
return;
}
gFolderDisplay.hintAboutToDeleteMessages();
const messengerBundle = document.getElementById("bundle_messenger");
let noSubjectString = messengerBundle.getString(
"defaultSaveMessageAsFileName"
);
if (noSubjectString.endsWith(".eml")) {
noSubjectString = noSubjectString.slice(0, -4);
}
const longSubjectTruncator = messengerBundle.getString(
"longMsgSubjectTruncator"
);
// Clip the subject string to 124 chars to avoid problems on Windows,
// see NS_MAX_FILEDESCRIPTOR in m-c/widget/windows/nsDataObj.cpp .
const maxUncutNameLength = 124;
const maxCutNameLength = maxUncutNameLength - longSubjectTruncator.length;
const messages = new Map();
for (const [index, msgUri] of messageUris.entries()) {
const msgService = MailServices.messageServiceFromURI(msgUri);
const msgHdr = msgService.messageURIToMsgHdr(msgUri);
let subject = msgHdr.mime2DecodedSubject || "";
if (msgHdr.flags & Ci.nsMsgMessageFlags.HasRe) {
subject = "Re: " + subject;
}
let uniqueFileName;
// If there is no subject, use a default name.
// If subject needs to be truncated, add a truncation character to indicate it.
if (!subject) {
uniqueFileName = noSubjectString;
} else {
uniqueFileName =
subject.length <= maxUncutNameLength
? subject
: subject.substr(0, maxCutNameLength) + longSubjectTruncator;
}
let msgFileName = validateFileName(uniqueFileName);
let msgFileNameLowerCase = msgFileName.toLocaleLowerCase();
// @see https://github.com/eslint/eslint/issues/17807
// eslint-disable-next-line no-constant-condition
while (true) {
if (!messages[msgFileNameLowerCase]) {
messages[msgFileNameLowerCase] = 1;
break;
} else {
const postfix = "-" + messages[msgFileNameLowerCase];
messages[msgFileNameLowerCase]++;
msgFileName = msgFileName + postfix;
msgFileNameLowerCase = msgFileNameLowerCase + postfix;
}
}
msgFileName = msgFileName + ".eml";
const msgUrl = msgService.getUrlForUri(msgUri);
const separator = msgUrl.spec.includes("?") ? "&" : "?";
aEvent.dataTransfer.mozSetDataAt("text/x-moz-message", msgUri, index);
aEvent.dataTransfer.mozSetDataAt("text/x-moz-url", msgUrl.spec, index);
aEvent.dataTransfer.mozSetDataAt(
"application/x-moz-file-promise-url",
msgUrl.spec + separator + "fileName=" + encodeURIComponent(msgFileName),
index
);
aEvent.dataTransfer.mozSetDataAt(
"application/x-moz-file-promise",
new messageFlavorDataProvider(),
index
);
aEvent.dataTransfer.mozSetDataAt(
"application/x-moz-file-promise-dest-filename",
msgFileName.replace(/(.{74}).*(.{10})$/u, "$1...$2"),
index
);
}
aEvent.dataTransfer.effectAllowed = "copyMove";
aEvent.dataTransfer.addElement(aEvent.target);
}
function ThreadPaneOnClick(event) {
// We only care about button 0 (left click) events.
if (event.button != 0) {
event.stopPropagation();
return;
}
// We already handle marking as read/flagged/junk cyclers in nsMsgDBView.cpp
// so all we need to worry about here is doubleclicks and column header. We
// get here for clicks on the "treecol" (headers) and the "scrollbarbutton"
// (scrollbar buttons) and don't want those events to cause a doubleclick.
const t = event.target;
if (t.localName == "treecol") {
HandleColumnClick(t.id);
return;
}
if (t.localName != "treechildren") {
return;
}
const tree = GetThreadTree();
// Figure out what cell the click was in.
const treeCellInfo = tree.getCellAt(event.clientX, event.clientY);
if (treeCellInfo.row == -1) {
return;
}
if (treeCellInfo.col.id == "selectCol") {
HandleSelectColClick(event, treeCellInfo.row);
return;
}
if (treeCellInfo.col.id == "deleteCol") {
handleDeleteColClick(event);
return;
}
// Cyclers and twisties respond to single clicks, not double clicks.
if (
event.detail == 2 &&
!treeCellInfo.col.cycler &&
treeCellInfo.childElt != "twisty"
) {
ThreadPaneDoubleClick();
}
}
function HandleColumnClick(columnID) {
if (columnID == "selectCol") {
const treeView = gFolderDisplay.tree.view;
const selection = treeView.selection;
if (!selection) {
return;
}
if (selection.count > 0) {
selection.clearSelection();
} else {
selection.selectAll();
}
return;
}
if (gFolderDisplay.BUILTIN_NOSORT_COLUMNS.has(columnID)) {
return;
}
if (!gFolderDisplay.BUILTIN_SORT_COLUMNS.has(columnID)) {
// This must be a custom column, check if it exists and is sortable.
const customColumn = ThreadPaneColumns.getCustomColumns().find(
c => c.id == columnID
);
if (!customColumn) {
dump(
`HandleColumnClick: No custom column handler registered for columnID: ${columnID}\n`
);
return;
}
if (!customColumn.sortKey) {
return;
}
}
if (gFolderDisplay.view.primarySortColumnId == columnID) {
MsgReverseSortThreadPane();
} else {
MsgSortThreadPane(columnID);
}
}
function HandleSelectColClick(event, row) {
// User wants to multiselect using the old way.
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
const tree = gFolderDisplay.tree;
const selection = tree.view.selection;
if (event.detail == 1) {
selection.toggleSelect(row);
}
// There is no longer any selection, clean up for correct state of things.
if (selection.count == 0) {
if (gFolderDisplay.displayedFolder) {
gFolderDisplay.displayedFolder.lastMessageLoaded = nsMsgKey_None;
}
gFolderDisplay._mostRecentSelectionCounts[1] = 0;
}
}
/**
* Delete a message without selecting it or loading its content.
*
* @param {DOMEvent} event - The DOM Event.
*/
function handleDeleteColClick(event) {
// Prevent deletion if any of the modifier keys was pressed.
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
// Simulate a right click on the message row to inherit all the validations
// and alerts coming from the "cmd_delete" command.
ChangeSelectionWithoutContentLoad(
event,
event.target.parentNode,
event.button == 1
);
// Trigger the message deletion.
goDoCommand("cmd_delete");
}
function ThreadPaneDoubleClick() {
MsgOpenSelectedMessages();
}
function ThreadPaneKeyDown(event) {
if (event.keyCode != KeyEvent.DOM_VK_RETURN) {
return;
}
// Prevent any thread that happens to be last selected (currentIndex) in a
// single or multi selection from toggling in tree.js.
event.stopImmediatePropagation();
ThreadPaneDoubleClick();
}
function MsgSortThreadPane(columnId) {
gFolderDisplay.view._threadExpandAll = Boolean(
gFolderDisplay.view._viewFlags & Ci.nsMsgViewFlagsType.kExpandAll
);
gFolderDisplay.view.sort(columnId, Ci.nsMsgViewSortOrder.ascending);
}
function MsgReverseSortThreadPane() {
gFolderDisplay.view._threadExpandAll = Boolean(
gFolderDisplay.view._viewFlags & Ci.nsMsgViewFlagsType.kExpandAll
);
if (gFolderDisplay.view.isSortedAscending) {
gFolderDisplay.view.sortDescending();
} else {
gFolderDisplay.view.sortAscending();
}
}
// XXX this should probably migrate into FolderDisplayWidget, or whatever
// FolderDisplayWidget ends up using if it refactors column management out.
function UpdateSortIndicators(colID, sortOrder) {
// Remove the sort indicator from all the columns
const treeColumns = document.getElementById("threadCols").children;
for (let i = 0; i < treeColumns.length; i++) {
treeColumns[i].removeAttribute("sortDirection");
}
let sortedColumn;
// set the sort indicator on the column we are sorted by
if (colID) {
sortedColumn = document.getElementById(colID);
}
if (sortedColumn) {
sortedColumn.setAttribute(
"sortDirection",
sortOrder == Ci.nsMsgViewSortOrder.ascending ? "ascending" : "descending"
);
}
}
function GetThreadTree() {
return document.getElementById("threadTree");
}
function ThreadPaneOnLoad() {
var tree = GetThreadTree();
// We won't have the tree if we're in a message window, so exit silently
if (!tree) {
return;
}
tree.addEventListener("click", ThreadPaneOnClick, true);
tree.addEventListener(
"dblclick",
event => {
// The tree.js dblclick event handler is handling editing and toggling
// open state of the cell. We don't use editing, and we want to handle
// the toggling through the click handler (also for double click), so
// capture the dblclick event before it bubbles up and causes the
// tree.js dblclick handler to toggle open state.
event.stopPropagation();
},
true
);
}
function ThreadPaneSelectionChanged() {
GetThreadTree().view.selectionChanged();
UpdateSelectCol();
UpdateMailSearch();
}
function UpdateSelectCol() {
const selectCol = document.getElementById("selectCol");
if (!selectCol) {
return;
}
const treeView = gFolderDisplay.tree.view;
const selection = treeView.selection;
if (selection && selection.count > 0) {
if (treeView.rowCount == selection.count) {
selectCol.classList.remove("someselected");
selectCol.classList.add("allselected");
} else {
selectCol.classList.remove("allselected");
selectCol.classList.add("someselected");
}
} else {
selectCol.classList.remove("allselected");
selectCol.classList.remove("someselected");
}
}
addEventListener("load", ThreadPaneOnLoad, true);