summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/compose/content
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/compose/content
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/compose/content')
-rw-r--r--comm/mail/components/compose/content/ComposerCommands.js2261
-rw-r--r--comm/mail/components/compose/content/MsgComposeCommands.js11654
-rw-r--r--comm/mail/components/compose/content/addressingWidgetOverlay.js1336
-rw-r--r--comm/mail/components/compose/content/bigFileObserver.js368
-rw-r--r--comm/mail/components/compose/content/cloudAttachmentLinkManager.js758
-rw-r--r--comm/mail/components/compose/content/dialogs/EdAEAttributes.js973
-rw-r--r--comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js146
-rw-r--r--comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js362
-rw-r--r--comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js200
-rw-r--r--comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js342
-rw-r--r--comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml243
-rw-r--r--comm/mail/components/compose/content/dialogs/EdColorPicker.js290
-rw-r--r--comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml103
-rw-r--r--comm/mail/components/compose/content/dialogs/EdColorProps.js476
-rw-r--r--comm/mail/components/compose/content/dialogs/EdColorProps.xhtml211
-rw-r--r--comm/mail/components/compose/content/dialogs/EdConvertToTable.js325
-rw-r--r--comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml86
-rw-r--r--comm/mail/components/compose/content/dialogs/EdDialogCommon.js679
-rw-r--r--comm/mail/components/compose/content/dialogs/EdDictionary.js138
-rw-r--r--comm/mail/components/compose/content/dialogs/EdDictionary.xhtml88
-rw-r--r--comm/mail/components/compose/content/dialogs/EdHLineProps.js227
-rw-r--r--comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml131
-rw-r--r--comm/mail/components/compose/content/dialogs/EdImageDialog.js639
-rw-r--r--comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js144
-rw-r--r--comm/mail/components/compose/content/dialogs/EdImageProps.js293
-rw-r--r--comm/mail/components/compose/content/dialogs/EdImageProps.xhtml454
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsSrc.js162
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml67
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertChars.js412
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml92
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertMath.js317
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml73
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertTOC.js378
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml505
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertTable.js258
-rw-r--r--comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml126
-rw-r--r--comm/mail/components/compose/content/dialogs/EdLinkProps.js323
-rw-r--r--comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml112
-rw-r--r--comm/mail/components/compose/content/dialogs/EdListProps.js455
-rw-r--r--comm/mail/components/compose/content/dialogs/EdListProps.xhtml101
-rw-r--r--comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js159
-rw-r--r--comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml67
-rw-r--r--comm/mail/components/compose/content/dialogs/EdReplace.js380
-rw-r--r--comm/mail/components/compose/content/dialogs/EdReplace.xhtml126
-rw-r--r--comm/mail/components/compose/content/dialogs/EdSpellCheck.js496
-rw-r--r--comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml209
-rw-r--r--comm/mail/components/compose/content/dialogs/EdTableProps.js1426
-rw-r--r--comm/mail/components/compose/content/dialogs/EdTableProps.xhtml472
-rw-r--r--comm/mail/components/compose/content/editFormatButtons.inc.xhtml282
-rw-r--r--comm/mail/components/compose/content/editor.js2392
-rw-r--r--comm/mail/components/compose/content/editorUtilities.js1015
-rw-r--r--comm/mail/components/compose/content/images/tag-anchor.gifbin0 -> 127 bytes
-rw-r--r--comm/mail/components/compose/content/messengercompose.xhtml2572
53 files changed, 35904 insertions, 0 deletions
diff --git a/comm/mail/components/compose/content/ComposerCommands.js b/comm/mail/components/compose/content/ComposerCommands.js
new file mode 100644
index 0000000000..7e9d7a992d
--- /dev/null
+++ b/comm/mail/components/compose/content/ComposerCommands.js
@@ -0,0 +1,2261 @@
+/* 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/. */
+
+/**
+ * Implementations of nsIControllerCommand for composer commands. These commands
+ * are related to editing. You can fire these commands with following functions:
+ * goDoCommand and goDoCommandParams(If command requires any parameters).
+ *
+ * Sometimes, we want to reflect the changes in the UI also. We have two functions
+ * for that: pokeStyleUI and pokeMultiStateUI. The pokeStyleUI function is for those
+ * commands which are boolean in nature for example "cmd_bold" command, text can
+ * be bold or not. The pokeMultiStateUI function is for the commands which can have
+ * multiple values for example "cmd_fontFace" can have different values like
+ * arial, variable width etc.
+ *
+ * Here, some of the commands are getting executed by document.execCommand.
+ * Those are listed in the gCommandMap Map object. In that also, some commands
+ * are of type boolean and some are of multiple state. We have two functions to
+ * execute them: doStatefulCommand and doStyleUICommand.
+ *
+ * All commands are not executable through document.execCommand.
+ * In all those cases, we will use goDoCommand or goDoCommandParams.
+ * The goDoCommandParams function is implemented in this file.
+ * The goDoCOmmand function is from globalOverlay.js. For the Commands
+ * which can be executed by document.execCommand, we will use doStatefulCommand
+ * and doStyleUICommand.
+ */
+
+/* import-globals-from ../../../../../toolkit/components/printing/content/printUtils.js */
+/* import-globals-from ../../../base/content/globalOverlay.js */
+/* import-globals-from ../../../base/content/utilityOverlay.js */
+/* import-globals-from editor.js */
+/* import-globals-from editorUtilities.js */
+/* import-globals-from MsgComposeCommands.js */
+
+var gComposerJSCommandControllerID = 0;
+
+/**
+ * Used to register commands we have created manually.
+ */
+function SetupHTMLEditorCommands() {
+ var commandTable = GetComposerCommandTable();
+ if (!commandTable) {
+ return;
+ }
+
+ // Include everything a text editor does
+ SetupTextEditorCommands();
+
+ // dump("Registering HTML editor commands\n");
+
+ commandTable.registerCommand("cmd_renderedHTMLEnabler", nsDummyHTMLCommand);
+
+ commandTable.registerCommand("cmd_listProperties", nsListPropertiesCommand);
+ commandTable.registerCommand("cmd_colorProperties", nsColorPropertiesCommand);
+ commandTable.registerCommand("cmd_increaseFontStep", nsIncreaseFontCommand);
+ commandTable.registerCommand("cmd_decreaseFontStep", nsDecreaseFontCommand);
+ commandTable.registerCommand(
+ "cmd_objectProperties",
+ nsObjectPropertiesCommand
+ );
+ commandTable.registerCommand(
+ "cmd_removeNamedAnchors",
+ nsRemoveNamedAnchorsCommand
+ );
+
+ commandTable.registerCommand("cmd_image", nsImageCommand);
+ commandTable.registerCommand("cmd_hline", nsHLineCommand);
+ commandTable.registerCommand("cmd_link", nsLinkCommand);
+ commandTable.registerCommand("cmd_anchor", nsAnchorCommand);
+ commandTable.registerCommand(
+ "cmd_insertHTMLWithDialog",
+ nsInsertHTMLWithDialogCommand
+ );
+ commandTable.registerCommand(
+ "cmd_insertMathWithDialog",
+ nsInsertMathWithDialogCommand
+ );
+ commandTable.registerCommand("cmd_insertBreakAll", nsInsertBreakAllCommand);
+
+ commandTable.registerCommand("cmd_table", nsInsertOrEditTableCommand);
+ commandTable.registerCommand("cmd_editTable", nsEditTableCommand);
+ commandTable.registerCommand("cmd_SelectTable", nsSelectTableCommand);
+ commandTable.registerCommand("cmd_SelectRow", nsSelectTableRowCommand);
+ commandTable.registerCommand("cmd_SelectColumn", nsSelectTableColumnCommand);
+ commandTable.registerCommand("cmd_SelectCell", nsSelectTableCellCommand);
+ commandTable.registerCommand(
+ "cmd_SelectAllCells",
+ nsSelectAllTableCellsCommand
+ );
+ commandTable.registerCommand("cmd_InsertTable", nsInsertTableCommand);
+ commandTable.registerCommand(
+ "cmd_InsertRowAbove",
+ nsInsertTableRowAboveCommand
+ );
+ commandTable.registerCommand(
+ "cmd_InsertRowBelow",
+ nsInsertTableRowBelowCommand
+ );
+ commandTable.registerCommand(
+ "cmd_InsertColumnBefore",
+ nsInsertTableColumnBeforeCommand
+ );
+ commandTable.registerCommand(
+ "cmd_InsertColumnAfter",
+ nsInsertTableColumnAfterCommand
+ );
+ commandTable.registerCommand(
+ "cmd_InsertCellBefore",
+ nsInsertTableCellBeforeCommand
+ );
+ commandTable.registerCommand(
+ "cmd_InsertCellAfter",
+ nsInsertTableCellAfterCommand
+ );
+ commandTable.registerCommand("cmd_DeleteTable", nsDeleteTableCommand);
+ commandTable.registerCommand("cmd_DeleteRow", nsDeleteTableRowCommand);
+ commandTable.registerCommand("cmd_DeleteColumn", nsDeleteTableColumnCommand);
+ commandTable.registerCommand("cmd_DeleteCell", nsDeleteTableCellCommand);
+ commandTable.registerCommand(
+ "cmd_DeleteCellContents",
+ nsDeleteTableCellContentsCommand
+ );
+ commandTable.registerCommand("cmd_JoinTableCells", nsJoinTableCellsCommand);
+ commandTable.registerCommand("cmd_SplitTableCell", nsSplitTableCellCommand);
+ commandTable.registerCommand(
+ "cmd_TableOrCellColor",
+ nsTableOrCellColorCommand
+ );
+ commandTable.registerCommand("cmd_smiley", nsSetSmiley);
+ commandTable.registerCommand("cmd_ConvertToTable", nsConvertToTable);
+}
+
+function SetupTextEditorCommands() {
+ var commandTable = GetComposerCommandTable();
+ if (!commandTable) {
+ return;
+ }
+ // dump("Registering plain text editor commands\n");
+
+ commandTable.registerCommand("cmd_findReplace", nsFindReplaceCommand);
+ commandTable.registerCommand("cmd_find", nsFindCommand);
+ commandTable.registerCommand("cmd_findNext", nsFindAgainCommand);
+ commandTable.registerCommand("cmd_findPrev", nsFindAgainCommand);
+ commandTable.registerCommand("cmd_rewrap", nsRewrapCommand);
+ commandTable.registerCommand("cmd_spelling", nsSpellingCommand);
+ commandTable.registerCommand("cmd_insertChars", nsInsertCharsCommand);
+}
+
+/**
+ * Used to register the command controller in the editor document.
+ *
+ * @returns {nsIControllerCommandTable|null} - A controller used to
+ * register the manually created commands.
+ */
+function GetComposerCommandTable() {
+ var controller;
+ if (gComposerJSCommandControllerID) {
+ try {
+ controller = window.content.controllers.getControllerById(
+ gComposerJSCommandControllerID
+ );
+ } catch (e) {}
+ }
+ if (!controller) {
+ // create it
+ controller =
+ Cc["@mozilla.org/embedcomp/base-command-controller;1"].createInstance();
+
+ var editorController = controller.QueryInterface(Ci.nsIControllerContext);
+ editorController.setCommandContext(GetCurrentEditorElement());
+ window.content.controllers.insertControllerAt(0, controller);
+
+ // Store the controller ID so we can be sure to get the right one later
+ gComposerJSCommandControllerID =
+ window.content.controllers.getControllerId(controller);
+ }
+
+ if (controller) {
+ var interfaceRequestor = controller.QueryInterface(
+ Ci.nsIInterfaceRequestor
+ );
+ return interfaceRequestor.getInterface(Ci.nsIControllerCommandTable);
+ }
+ return null;
+}
+
+/* eslint-disable complexity */
+
+/**
+ * Get the state of the given command and call the pokeStyleUI or pokeMultiStateUI
+ * according to the type of the command to reflect the UI changes in the editor.
+ *
+ * @param {string} command - The id of the command.
+ */
+function goUpdateCommandState(command) {
+ try {
+ var controller =
+ document.commandDispatcher.getControllerForCommand(command);
+ if (!(controller instanceof Ci.nsICommandController)) {
+ return;
+ }
+
+ var params = newCommandParams();
+ if (!params) {
+ return;
+ }
+
+ controller.getCommandStateWithParams(command, params);
+
+ switch (command) {
+ case "cmd_bold":
+ case "cmd_italic":
+ case "cmd_underline":
+ case "cmd_var":
+ case "cmd_samp":
+ case "cmd_code":
+ case "cmd_acronym":
+ case "cmd_abbr":
+ case "cmd_cite":
+ case "cmd_strong":
+ case "cmd_em":
+ case "cmd_superscript":
+ case "cmd_subscript":
+ case "cmd_strikethrough":
+ case "cmd_tt":
+ case "cmd_nobreak":
+ case "cmd_ul":
+ case "cmd_ol":
+ pokeStyleUI(command, params.getBooleanValue("state_all"));
+ break;
+
+ case "cmd_paragraphState":
+ case "cmd_align":
+ case "cmd_highlight":
+ case "cmd_backgroundColor":
+ case "cmd_fontColor":
+ case "cmd_fontFace":
+ pokeMultiStateUI(command, params);
+ break;
+
+ case "cmd_indent":
+ case "cmd_outdent":
+ case "cmd_increaseFont":
+ case "cmd_decreaseFont":
+ case "cmd_increaseFontStep":
+ case "cmd_decreaseFontStep":
+ case "cmd_removeStyles":
+ case "cmd_smiley":
+ break;
+
+ default:
+ dump("no update for command: " + command + "\n");
+ }
+ } catch (e) {
+ console.error(e);
+ }
+}
+/* eslint-enable complexity */
+
+/**
+ * Used in the oncommandupdate attribute of the goUpdateComposerMenuItems.
+ * For any commandset events fired, this function will be called.
+ * Used to update the UI state of the editor buttons and menulist.
+ * Whenever you change your selection in the editor part, i.e. if you move
+ * your cursor, you will find this functions getting called and
+ * updating the editor UI of toolbarbuttons and menulists. This is mainly
+ * to update the UI according to your selection in the editor part.
+ *
+ * @param {XULElement} commandset - The <xul:commandset> element to update for.
+ */
+function goUpdateComposerMenuItems(commandset) {
+ // dump("Updating commands for " + commandset.id + "\n");
+ for (var i = 0; i < commandset.children.length; i++) {
+ var commandNode = commandset.children[i];
+ var commandID = commandNode.id;
+ if (commandID) {
+ goUpdateCommand(commandID); // enable or disable
+ if (commandNode.hasAttribute("state")) {
+ goUpdateCommandState(commandID);
+ }
+ }
+ }
+}
+
+/**
+ * Execute the command with the provided parameters.
+ * This is directly calling commands with multiple state attributes, which
+ * are not supported by document.execCommand()
+ *
+ * @param {string} command - The command ID.
+ * @param {string} paramValue - The parameter value.
+ */
+function goDoCommandParams(command, paramValue) {
+ try {
+ let params = newCommandParams();
+ params.setStringValue("state_attribute", paramValue);
+ let controller =
+ document.commandDispatcher.getControllerForCommand(command);
+ if (controller && controller.isCommandEnabled(command)) {
+ if (controller instanceof Ci.nsICommandController) {
+ controller.doCommandWithParams(command, params);
+ } else {
+ controller.doCommand(command);
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+/**
+ * Update the UI to reflect setting a given state for a command. This
+ * is used for boolean type of commands.
+ *
+ * @param {string} uiID - The id of the command.
+ * @param {boolean} desiredState - State to set for the command.
+ */
+function pokeStyleUI(uiID, desiredState) {
+ let commandNode = document.getElementById(uiID);
+ let uiState = commandNode.getAttribute("state") == "true";
+ if (desiredState != uiState) {
+ commandNode.setAttribute("state", desiredState ? "true" : "false");
+ let buttonId;
+ switch (uiID) {
+ case "cmd_bold":
+ buttonId = "boldButton";
+ break;
+ case "cmd_italic":
+ buttonId = "italicButton";
+ break;
+ case "cmd_underline":
+ buttonId = "underlineButton";
+ break;
+ case "cmd_ul":
+ buttonId = "ulButton";
+ break;
+ case "cmd_ol":
+ buttonId = "olButton";
+ break;
+ }
+ if (buttonId) {
+ document.getElementById(buttonId).checked = desiredState;
+ }
+ }
+}
+
+/**
+ * Maps internal command names to their document.execCommand() command string.
+ */
+let gCommandMap = new Map([
+ ["cmd_bold", "bold"],
+ ["cmd_italic", "italic"],
+ ["cmd_underline", "underline"],
+ ["cmd_strikethrough", "strikethrough"],
+ ["cmd_superscript", "superscript"],
+ ["cmd_subscript", "subscript"],
+ ["cmd_ul", "InsertUnorderedList"],
+ ["cmd_ol", "InsertOrderedList"],
+ ["cmd_fontFace", "fontName"],
+
+ // This are currently implemented with the help of
+ // color selection dialog box in the editor.js.
+ // ["cmd_highlight", "backColor"],
+ // ["cmd_fontColor", "foreColor"],
+]);
+
+/**
+ * Used for the boolean type commands available through
+ * document.execCommand(). We will also call pokeStyleUI to update
+ * the UI.
+ *
+ * @param {string} cmdStr - The id of the command.
+ */
+function doStyleUICommand(cmdStr) {
+ GetCurrentEditorElement().contentDocument.execCommand(
+ gCommandMap.get(cmdStr),
+ false,
+ null
+ );
+ let commandNode = document.getElementById(cmdStr);
+ let newState = commandNode.getAttribute("state") != "true";
+ pokeStyleUI(cmdStr, newState);
+}
+
+// Copied from jsmime.js.
+function stringToTypedArray(buffer) {
+ var typedarray = new Uint8Array(buffer.length);
+ for (var i = 0; i < buffer.length; i++) {
+ typedarray[i] = buffer.charCodeAt(i);
+ }
+ return typedarray;
+}
+
+/**
+ * Update the UI to reflect setting a given state for a command. This is used
+ * when the command state has a string value i.e. multiple state type commands.
+ *
+ * @param {string} uiID - The id of the command.
+ * @param {nsICommandParams} cmdParams - Command parameters object.
+ */
+function pokeMultiStateUI(uiID, cmdParams) {
+ let desiredAttrib;
+ if (cmdParams.getBooleanValue("state_mixed")) {
+ desiredAttrib = "mixed";
+ } else if (
+ cmdParams.getValueType("state_attribute") == Ci.nsICommandParams.eStringType
+ ) {
+ desiredAttrib = cmdParams.getCStringValue("state_attribute");
+ // Decode UTF-8, for example for font names in Japanese.
+ desiredAttrib = new TextDecoder("UTF-8").decode(
+ stringToTypedArray(desiredAttrib)
+ );
+ } else {
+ desiredAttrib = cmdParams.getStringValue("state_attribute");
+ }
+
+ let commandNode = document.getElementById(uiID);
+ let uiState = commandNode.getAttribute("state");
+ if (desiredAttrib != uiState) {
+ commandNode.setAttribute("state", desiredAttrib);
+ switch (uiID) {
+ case "cmd_paragraphState": {
+ onParagraphFormatChange();
+ break;
+ }
+ case "cmd_fontFace": {
+ onFontFaceChange();
+ break;
+ }
+ case "cmd_fontColor": {
+ onFontColorChange();
+ break;
+ }
+ case "cmd_backgroundColor": {
+ onBackgroundColorChange();
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Perform the action of the multiple states type commands available through
+ * document.execCommand().
+ *
+ * @param {string} commandID - The id of the command.
+ * @param {string} newState - The parameter value.
+ * @param {boolean} updateUI - updates the UI if true. Used when
+ * function is called in another JavaScript function.
+ */
+function doStatefulCommand(commandID, newState, updateUI) {
+ if (commandID == "cmd_align") {
+ let command;
+ switch (newState) {
+ case "left":
+ command = "justifyLeft";
+ break;
+ case "center":
+ command = "justifyCenter";
+ break;
+ case "right":
+ command = "justifyRight";
+ break;
+ case "justify":
+ command = "justifyFull";
+ break;
+ }
+ GetCurrentEditorElement().contentDocument.execCommand(command, false, null);
+ } else if (commandID == "cmd_fontFace" && newState == "") {
+ goDoCommandParams(commandID, newState);
+ } else {
+ GetCurrentEditorElement().contentDocument.execCommand(
+ gCommandMap.get(commandID),
+ false,
+ newState
+ );
+ }
+
+ if (updateUI) {
+ let commandNode = document.getElementById(commandID);
+ commandNode.setAttribute("state", newState);
+ switch (commandID) {
+ case "cmd_fontFace": {
+ onFontFaceChange();
+ break;
+ }
+ }
+ } else {
+ let commandNode = document.getElementById(commandID);
+ if (commandNode) {
+ commandNode.setAttribute("state", newState);
+ }
+ }
+}
+
+var nsDummyHTMLCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ // do nothing
+ dump("Hey, who's calling the dummy command?\n");
+ },
+};
+
+// ------- output utilities ----- //
+
+// returns a fileExtension string
+function GetExtensionBasedOnMimeType(aMIMEType) {
+ try {
+ var mimeService = null;
+ mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+ var fileExtension = mimeService.getPrimaryExtension(aMIMEType, null);
+
+ // the MIME service likes to give back ".htm" for text/html files,
+ // so do a special-case fix here.
+ if (fileExtension == "htm") {
+ fileExtension = "html";
+ }
+
+ return fileExtension;
+ } catch (e) {}
+ return "";
+}
+
+function GetSuggestedFileName(aDocumentURLString, aMIMEType) {
+ var extension = GetExtensionBasedOnMimeType(aMIMEType);
+ if (extension) {
+ extension = "." + extension;
+ }
+
+ // check for existing file name we can use
+ if (aDocumentURLString && !IsUrlAboutBlank(aDocumentURLString)) {
+ try {
+ let docURI = Services.io.newURI(
+ aDocumentURLString,
+ GetCurrentEditor().documentCharacterSet
+ );
+ docURI = docURI.QueryInterface(Ci.nsIURL);
+
+ // grab the file name
+ let url = validateFileName(decodeURIComponent(docURI.fileBaseName));
+ if (url) {
+ return url + extension;
+ }
+ } catch (e) {}
+ }
+
+ // Check if there is a title we can use to generate a valid filename,
+ // if we can't, use the default filename.
+ var title =
+ validateFileName(GetDocumentTitle()) ||
+ GetString("untitledDefaultFilename");
+ return title + extension;
+}
+
+/**
+ * @returns {Promise} dialogResult
+ */
+function PromptForSaveLocation(
+ aDoSaveAsText,
+ aEditorType,
+ aMIMEType,
+ aDocumentURLString
+) {
+ var dialogResult = {};
+ dialogResult.filepickerClick = Ci.nsIFilePicker.returnCancel;
+ dialogResult.resultingURI = "";
+ dialogResult.resultingLocalFile = null;
+
+ var fp = null;
+ try {
+ fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ } catch (e) {}
+ if (!fp) {
+ return dialogResult;
+ }
+
+ // determine prompt string based on type of saving we'll do
+ var promptString;
+ if (aDoSaveAsText || aEditorType == "text") {
+ promptString = GetString("SaveTextAs");
+ } else {
+ promptString = GetString("SaveDocumentAs");
+ }
+
+ fp.init(window, promptString, Ci.nsIFilePicker.modeSave);
+
+ // Set filters according to the type of output
+ if (aDoSaveAsText) {
+ fp.appendFilters(Ci.nsIFilePicker.filterText);
+ } else {
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ }
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ // now let's actually set the filepicker's suggested filename
+ var suggestedFileName = GetSuggestedFileName(aDocumentURLString, aMIMEType);
+ if (suggestedFileName) {
+ fp.defaultString = suggestedFileName;
+ }
+
+ // set the file picker's current directory
+ // assuming we have information needed (like prior saved location)
+ try {
+ var fileHandler = GetFileProtocolHandler();
+
+ var isLocalFile = true;
+ try {
+ let docURI = Services.io.newURI(
+ aDocumentURLString,
+ GetCurrentEditor().documentCharacterSet
+ );
+ isLocalFile = docURI.schemeIs("file");
+ } catch (e) {}
+
+ var parentLocation = null;
+ if (isLocalFile) {
+ var fileLocation = fileHandler.getFileFromURLSpec(aDocumentURLString); // this asserts if url is not local
+ parentLocation = fileLocation.parent;
+ }
+ if (parentLocation) {
+ // Save current filepicker's default location
+ if ("gFilePickerDirectory" in window) {
+ gFilePickerDirectory = fp.displayDirectory;
+ }
+
+ fp.displayDirectory = parentLocation;
+ } else {
+ // Initialize to the last-used directory for the particular type (saved in prefs)
+ SetFilePickerDirectory(fp, aEditorType);
+ }
+ } catch (e) {}
+
+ return new Promise(resolve => {
+ fp.open(rv => {
+ dialogResult.filepickerClick = rv;
+ if (rv != Ci.nsIFilePicker.returnCancel && fp.file) {
+ // Allow OK and replace.
+ // reset urlstring to new save location
+ dialogResult.resultingURIString = fileHandler.getURLSpecFromActualFile(
+ fp.file
+ );
+ dialogResult.resultingLocalFile = fp.file;
+ SaveFilePickerDirectory(fp, aEditorType);
+ resolve(dialogResult);
+ } else if ("gFilePickerDirectory" in window && gFilePickerDirectory) {
+ fp.displayDirectory = gFilePickerDirectory;
+ resolve(null);
+ }
+ });
+ });
+}
+
+/**
+ * If needed, prompt for document title and set the document title to the
+ * preferred value.
+ *
+ * @returns true if the title was set up successfully;
+ * false if the user cancelled the title prompt
+ */
+function PromptAndSetTitleIfNone() {
+ if (GetDocumentTitle()) {
+ // we have a title; no need to prompt!
+ return true;
+ }
+
+ let result = { value: null };
+ let captionStr = GetString("DocumentTitle");
+ let msgStr = GetString("NeedDocTitle") + "\n" + GetString("DocTitleHelp");
+ let confirmed = Services.prompt.prompt(
+ window,
+ captionStr,
+ msgStr,
+ result,
+ null,
+ { value: 0 }
+ );
+ if (confirmed) {
+ SetDocumentTitle(TrimString(result.value));
+ }
+
+ return confirmed;
+}
+
+var gPersistObj;
+
+// Don't forget to do these things after calling OutputFileWithPersistAPI:
+// we need to update the uri before notifying listeners
+// UpdateWindowTitle();
+// if (!aSaveCopy)
+// editor.resetModificationCount();
+// this should cause notification to listeners that document has changed
+
+function OutputFileWithPersistAPI(
+ editorDoc,
+ aDestinationLocation,
+ aRelatedFilesParentDir,
+ aMimeType
+) {
+ gPersistObj = null;
+ var editor = GetCurrentEditor();
+ try {
+ editor.forceCompositionEnd();
+ } catch (e) {}
+
+ var isLocalFile = false;
+ try {
+ aDestinationLocation.QueryInterface(Ci.nsIFile);
+ isLocalFile = true;
+ } catch (e) {
+ try {
+ var tmp = aDestinationLocation.QueryInterface(Ci.nsIURI);
+ isLocalFile = tmp.schemeIs("file");
+ } catch (e) {}
+ }
+
+ try {
+ // we should supply a parent directory if/when we turn on functionality to save related documents
+ var persistObj = Cc[
+ "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"
+ ].createInstance(Ci.nsIWebBrowserPersist);
+ persistObj.progressListener = gEditorOutputProgressListener;
+
+ var wrapColumn = GetWrapColumn();
+ var outputFlags = GetOutputFlags(aMimeType, wrapColumn);
+
+ // for 4.x parity as well as improving readability of file locally on server
+ // this will always send crlf for upload (http/ftp)
+ if (!isLocalFile) {
+ // if we aren't saving locally then send both cr and lf
+ outputFlags |=
+ Ci.nsIWebBrowserPersist.ENCODE_FLAGS_CR_LINEBREAKS |
+ Ci.nsIWebBrowserPersist.ENCODE_FLAGS_LF_LINEBREAKS;
+
+ // we want to serialize the output for all remote publishing
+ // some servers can handle only one connection at a time
+ // some day perhaps we can make this user-configurable per site?
+ persistObj.persistFlags =
+ persistObj.persistFlags |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_SERIALIZE_OUTPUT;
+ }
+
+ // note: we always want to set the replace existing files flag since we have
+ // already given user the chance to not replace an existing file (file picker)
+ // or the user picked an option where the file is implicitly being replaced (save)
+ persistObj.persistFlags =
+ persistObj.persistFlags |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_BASE_TAG_MODIFICATIONS |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_DONT_FIXUP_LINKS |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_DONT_CHANGE_FILENAMES |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FIXUP_ORIGINAL_DOM;
+ persistObj.saveDocument(
+ editorDoc,
+ aDestinationLocation,
+ aRelatedFilesParentDir,
+ aMimeType,
+ outputFlags,
+ wrapColumn
+ );
+ gPersistObj = persistObj;
+ } catch (e) {
+ dump("caught an error, bail\n");
+ return false;
+ }
+
+ return true;
+}
+
+// returns output flags based on mimetype, wrapCol and prefs
+function GetOutputFlags(aMimeType, aWrapColumn) {
+ var outputFlags = 0;
+ var editor = GetCurrentEditor();
+ var outputEntity =
+ editor && editor.documentCharacterSet == "ISO-8859-1"
+ ? Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_LATIN1_ENTITIES
+ : Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
+ if (aMimeType == "text/plain") {
+ // When saving in "text/plain" format, always do formatting
+ outputFlags |= Ci.nsIWebBrowserPersist.ENCODE_FLAGS_FORMATTED;
+ } else {
+ // Should we prettyprint? Check the pref
+ if (Services.prefs.getBoolPref("editor.prettyprint")) {
+ outputFlags |= Ci.nsIWebBrowserPersist.ENCODE_FLAGS_FORMATTED;
+ }
+
+ try {
+ // How much entity names should we output? Check the pref
+ switch (Services.prefs.getCharPref("editor.encode_entity")) {
+ case "basic":
+ outputEntity =
+ Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
+ break;
+ case "latin1":
+ outputEntity =
+ Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_LATIN1_ENTITIES;
+ break;
+ case "html":
+ outputEntity =
+ Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_HTML_ENTITIES;
+ break;
+ case "none":
+ outputEntity = 0;
+ break;
+ }
+ } catch (e) {}
+ }
+ outputFlags |= outputEntity;
+
+ if (aWrapColumn > 0) {
+ outputFlags |= Ci.nsIWebBrowserPersist.ENCODE_FLAGS_WRAP;
+ }
+
+ return outputFlags;
+}
+
+// returns number of column where to wrap
+function GetWrapColumn() {
+ try {
+ return GetCurrentEditor().wrapWidth;
+ } catch (e) {}
+ return 0;
+}
+
+const gShowDebugOutputStateChange = false;
+const gShowDebugOutputProgress = false;
+const gShowDebugOutputStatusChange = false;
+
+const gShowDebugOutputLocationChange = false;
+const gShowDebugOutputSecurityChange = false;
+
+const kErrorBindingAborted = 2152398850;
+const kErrorBindingRedirected = 2152398851;
+const kFileNotFound = 2152857618;
+
+var gEditorOutputProgressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Use this to access onStateChange flags
+ var requestSpec;
+ try {
+ var channel = aRequest.QueryInterface(Ci.nsIChannel);
+ requestSpec = StripUsernamePasswordFromURI(channel.URI);
+ } catch (e) {
+ if (gShowDebugOutputStateChange) {
+ dump("***** onStateChange; NO REQUEST CHANNEL\n");
+ }
+ }
+
+ if (gShowDebugOutputStateChange) {
+ dump("\n***** onStateChange request: " + requestSpec + "\n");
+ dump(" state flags: ");
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ dump(" STATE_START, ");
+ }
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ dump(" STATE_STOP, ");
+ }
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ dump(" STATE_IS_NETWORK ");
+ }
+
+ dump(`\n * requestSpec=${requestSpec}, aStatus=${aStatus}\n`);
+
+ DumpDebugStatus(aStatus);
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ if (!gPersistObj) {
+ return;
+ }
+
+ if (gShowDebugOutputProgress) {
+ dump(
+ "\n onProgressChange: gPersistObj.result=" + gPersistObj.result + "\n"
+ );
+ try {
+ var channel = aRequest.QueryInterface(Ci.nsIChannel);
+ dump("***** onProgressChange request: " + channel.URI.spec + "\n");
+ } catch (e) {}
+ dump(
+ "***** self: " +
+ aCurSelfProgress +
+ " / " +
+ aMaxSelfProgress +
+ "\n"
+ );
+ dump(
+ "***** total: " +
+ aCurTotalProgress +
+ " / " +
+ aMaxTotalProgress +
+ "\n\n"
+ );
+
+ if (gPersistObj.currentState == gPersistObj.PERSIST_STATE_READY) {
+ dump(" Persister is ready to save data\n\n");
+ } else if (gPersistObj.currentState == gPersistObj.PERSIST_STATE_SAVING) {
+ dump(" Persister is saving data.\n\n");
+ } else if (
+ gPersistObj.currentState == gPersistObj.PERSIST_STATE_FINISHED
+ ) {
+ dump(" PERSISTER HAS FINISHED SAVING DATA\n\n\n");
+ }
+ }
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (gShowDebugOutputLocationChange) {
+ dump("***** onLocationChange: " + aLocation.spec + "\n");
+ try {
+ var channel = aRequest.QueryInterface(Ci.nsIChannel);
+ dump("***** request: " + channel.URI.spec + "\n");
+ } catch (e) {}
+ }
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (gShowDebugOutputStatusChange) {
+ dump("***** onStatusChange: " + aMessage + "\n");
+ try {
+ var channel = aRequest.QueryInterface(Ci.nsIChannel);
+ dump("***** request: " + channel.URI.spec + "\n");
+ } catch (e) {
+ dump(" couldn't get request\n");
+ }
+
+ DumpDebugStatus(aStatus);
+
+ if (gPersistObj) {
+ if (gPersistObj.currentState == gPersistObj.PERSIST_STATE_READY) {
+ dump(" Persister is ready to save data\n\n");
+ } else if (
+ gPersistObj.currentState == gPersistObj.PERSIST_STATE_SAVING
+ ) {
+ dump(" Persister is saving data.\n\n");
+ } else if (
+ gPersistObj.currentState == gPersistObj.PERSIST_STATE_FINISHED
+ ) {
+ dump(" PERSISTER HAS FINISHED SAVING DATA\n\n\n");
+ }
+ }
+ }
+ },
+
+ onSecurityChange(aWebProgress, aRequest, state) {
+ if (gShowDebugOutputSecurityChange) {
+ try {
+ var channel = aRequest.QueryInterface(Ci.nsIChannel);
+ dump("***** onSecurityChange request: " + channel.URI.spec + "\n");
+ } catch (e) {}
+ }
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+/* eslint-disable complexity */
+function DumpDebugStatus(aStatus) {
+ // see nsError.h and netCore.h and ftpCore.h
+
+ if (aStatus == kErrorBindingAborted) {
+ dump("***** status is NS_BINDING_ABORTED\n");
+ } else if (aStatus == kErrorBindingRedirected) {
+ dump("***** status is NS_BINDING_REDIRECTED\n");
+ } else if (aStatus == 2152398859) {
+ // in netCore.h 11
+ dump("***** status is ALREADY_CONNECTED\n");
+ } else if (aStatus == 2152398860) {
+ // in netCore.h 12
+ dump("***** status is NOT_CONNECTED\n");
+ } else if (aStatus == 2152398861) {
+ // in nsISocketTransportService.idl 13
+ dump("***** status is CONNECTION_REFUSED\n");
+ } else if (aStatus == 2152398862) {
+ // in nsISocketTransportService.idl 14
+ dump("***** status is NET_TIMEOUT\n");
+ } else if (aStatus == 2152398863) {
+ // in netCore.h 15
+ dump("***** status is IN_PROGRESS\n");
+ } else if (aStatus == 2152398864) {
+ // 0x804b0010 in netCore.h 16
+ dump("***** status is OFFLINE\n");
+ } else if (aStatus == 2152398865) {
+ // in netCore.h 17
+ dump("***** status is NO_CONTENT\n");
+ } else if (aStatus == 2152398866) {
+ // in netCore.h 18
+ dump("***** status is UNKNOWN_PROTOCOL\n");
+ } else if (aStatus == 2152398867) {
+ // in netCore.h 19
+ dump("***** status is PORT_ACCESS_NOT_ALLOWED\n");
+ } else if (aStatus == 2152398868) {
+ // in nsISocketTransportService.idl 20
+ dump("***** status is NET_RESET\n");
+ } else if (aStatus == 2152398869) {
+ // in ftpCore.h 21
+ dump("***** status is FTP_LOGIN\n");
+ } else if (aStatus == 2152398870) {
+ // in ftpCore.h 22
+ dump("***** status is FTP_CWD\n");
+ } else if (aStatus == 2152398871) {
+ // in ftpCore.h 23
+ dump("***** status is FTP_PASV\n");
+ } else if (aStatus == 2152398872) {
+ // in ftpCore.h 24
+ dump("***** status is FTP_PWD\n");
+ } else if (aStatus == 2152857601) {
+ dump("***** status is UNRECOGNIZED_PATH\n");
+ } else if (aStatus == 2152857602) {
+ dump("***** status is UNRESOLABLE SYMLINK\n");
+ } else if (aStatus == 2152857604) {
+ dump("***** status is UNKNOWN_TYPE\n");
+ } else if (aStatus == 2152857605) {
+ dump("***** status is DESTINATION_NOT_DIR\n");
+ } else if (aStatus == 2152857606) {
+ dump("***** status is TARGET_DOES_NOT_EXIST\n");
+ } else if (aStatus == 2152857608) {
+ dump("***** status is ALREADY_EXISTS\n");
+ } else if (aStatus == 2152857609) {
+ dump("***** status is INVALID_PATH\n");
+ } else if (aStatus == 2152857610) {
+ dump("***** status is DISK_FULL\n");
+ } else if (aStatus == 2152857612) {
+ dump("***** status is NOT_DIRECTORY\n");
+ } else if (aStatus == 2152857613) {
+ dump("***** status is IS_DIRECTORY\n");
+ } else if (aStatus == 2152857614) {
+ dump("***** status is IS_LOCKED\n");
+ } else if (aStatus == 2152857615) {
+ dump("***** status is TOO_BIG\n");
+ } else if (aStatus == 2152857616) {
+ dump("***** status is NO_DEVICE_SPACE\n");
+ } else if (aStatus == 2152857617) {
+ dump("***** status is NAME_TOO_LONG\n");
+ } else if (aStatus == 2152857618) {
+ // 80520012
+ dump("***** status is FILE_NOT_FOUND\n");
+ } else if (aStatus == 2152857619) {
+ dump("***** status is READ_ONLY\n");
+ } else if (aStatus == 2152857620) {
+ dump("***** status is DIR_NOT_EMPTY\n");
+ } else if (aStatus == 2152857621) {
+ dump("***** status is ACCESS_DENIED\n");
+ } else if (aStatus == 2152398878) {
+ dump("***** status is ? (No connection or time out?)\n");
+ } else {
+ dump("***** status is " + aStatus + "\n");
+ }
+}
+/* eslint-enable complexity */
+
+const kSupportedTextMimeTypes = [
+ "text/plain",
+ "text/css",
+ "text/rdf",
+ "text/xsl",
+ "text/javascript", // obsolete type
+ "text/ecmascript", // obsolete type
+ "application/javascript",
+ "application/ecmascript",
+ "application/x-javascript", // obsolete type
+ "application/xhtml+xml",
+];
+
+function IsSupportedTextMimeType(aMimeType) {
+ for (var i = 0; i < kSupportedTextMimeTypes.length; i++) {
+ if (kSupportedTextMimeTypes[i] == aMimeType) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/* eslint-disable complexity */
+// throws an error or returns true if user attempted save; false if user canceled save
+async function SaveDocument(aSaveAs, aSaveCopy, aMimeType) {
+ var editor = GetCurrentEditor();
+ if (!aMimeType || !editor) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+
+ var editorDoc = editor.document;
+ if (!editorDoc) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+
+ // if we don't have the right editor type bail (we handle text and html)
+ var editorType = GetCurrentEditorType();
+ if (!["text", "html", "htmlmail", "textmail"].includes(editorType)) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ var saveAsTextFile = IsSupportedTextMimeType(aMimeType);
+
+ // check if the file is to be saved is a format we don't understand; if so, bail
+ if (
+ aMimeType != kHTMLMimeType &&
+ aMimeType != kXHTMLMimeType &&
+ !saveAsTextFile
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ if (saveAsTextFile) {
+ aMimeType = "text/plain";
+ }
+
+ var urlstring = GetDocumentUrl();
+ var mustShowFileDialog =
+ aSaveAs || IsUrlAboutBlank(urlstring) || urlstring == "";
+
+ // If editing a remote URL, force SaveAs dialog
+ if (!mustShowFileDialog && GetScheme(urlstring) != "file") {
+ mustShowFileDialog = true;
+ }
+
+ var doUpdateURI = false;
+ var tempLocalFile = null;
+
+ if (mustShowFileDialog) {
+ try {
+ // Prompt for title if we are saving to HTML
+ if (!saveAsTextFile && editorType == "html") {
+ var userContinuing = PromptAndSetTitleIfNone(); // not cancel
+ if (!userContinuing) {
+ return false;
+ }
+ }
+
+ var dialogResult = await PromptForSaveLocation(
+ saveAsTextFile,
+ editorType,
+ aMimeType,
+ urlstring
+ );
+ if (!dialogResult) {
+ return false;
+ }
+
+ // What is this unused 'replacing' var supposed to be doing?
+ /* eslint-disable-next-line no-unused-vars */
+ var replacing =
+ dialogResult.filepickerClick == Ci.nsIFilePicker.returnReplace;
+
+ urlstring = dialogResult.resultingURIString;
+ tempLocalFile = dialogResult.resultingLocalFile;
+
+ // update the new URL for the webshell unless we are saving a copy
+ if (!aSaveCopy) {
+ doUpdateURI = true;
+ }
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
+ } // mustShowFileDialog
+
+ var success = true;
+ try {
+ // if somehow we didn't get a local file but we did get a uri,
+ // attempt to create the localfile if it's a "file" url
+ var docURI;
+ if (!tempLocalFile) {
+ docURI = Services.io.newURI(urlstring, editor.documentCharacterSet);
+
+ if (docURI.schemeIs("file")) {
+ var fileHandler = GetFileProtocolHandler();
+ tempLocalFile = fileHandler
+ .getFileFromURLSpec(urlstring)
+ .QueryInterface(Ci.nsIFile);
+ }
+ }
+
+ // this is the location where the related files will go
+ var relatedFilesDir = null;
+
+ // Only change links or move files if pref is set
+ // and we are saving to a new location
+ if (Services.prefs.getBoolPref("editor.save_associated_files") && aSaveAs) {
+ try {
+ if (tempLocalFile) {
+ // if we are saving to the same parent directory, don't set relatedFilesDir
+ // grab old location, chop off file
+ // grab new location, chop off file, compare
+ var oldLocation = GetDocumentUrl();
+ var oldLocationLastSlash = oldLocation.lastIndexOf("/");
+ if (oldLocationLastSlash != -1) {
+ oldLocation = oldLocation.slice(0, oldLocationLastSlash);
+ }
+
+ var relatedFilesDirStr = urlstring;
+ var newLocationLastSlash = relatedFilesDirStr.lastIndexOf("/");
+ if (newLocationLastSlash != -1) {
+ relatedFilesDirStr = relatedFilesDirStr.slice(
+ 0,
+ newLocationLastSlash
+ );
+ }
+ if (
+ oldLocation == relatedFilesDirStr ||
+ IsUrlAboutBlank(oldLocation)
+ ) {
+ relatedFilesDir = null;
+ } else {
+ relatedFilesDir = tempLocalFile.parent;
+ }
+ } else {
+ var lastSlash = urlstring.lastIndexOf("/");
+ if (lastSlash != -1) {
+ var relatedFilesDirString = urlstring.slice(0, lastSlash + 1); // include last slash
+ relatedFilesDir = Services.io.newURI(
+ relatedFilesDirString,
+ editor.documentCharacterSet
+ );
+ }
+ }
+ } catch (e) {
+ relatedFilesDir = null;
+ }
+ }
+
+ let destinationLocation = tempLocalFile ? tempLocalFile : docURI;
+
+ success = OutputFileWithPersistAPI(
+ editorDoc,
+ destinationLocation,
+ relatedFilesDir,
+ aMimeType
+ );
+ } catch (e) {
+ success = false;
+ }
+
+ if (success) {
+ try {
+ if (doUpdateURI) {
+ // If a local file, we must create a new uri from nsIFile
+ if (tempLocalFile) {
+ docURI = GetFileProtocolHandler().newFileURI(tempLocalFile);
+ }
+ }
+
+ // Update window title to show possibly different filename
+ // This also covers problem that after undoing a title change,
+ // window title loses the extra [filename] part that this adds
+ UpdateWindowTitle();
+
+ if (!aSaveCopy) {
+ editor.resetModificationCount();
+ }
+ // this should cause notification to listeners that document has changed
+
+ // Set UI based on whether we're editing a remote or local url
+ goUpdateCommand("cmd_save");
+ } catch (e) {}
+ } else {
+ Services.prompt.alert(
+ window,
+ GetString("SaveDocument"),
+ GetString("SaveFileFailed")
+ );
+ }
+ return success;
+}
+/* eslint-enable complexity */
+
+var nsFindReplaceCommand = {
+ isCommandEnabled(aCommand, editorElement) {
+ return editorElement.getEditor(editorElement.contentWindow) != null;
+ },
+
+ getCommandStateParams(aCommand, aParams, editorElement) {},
+ doCommandParams(aCommand, aParams, editorElement) {},
+
+ doCommand(aCommand, editorElement) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdReplace.xhtml",
+ "_blank",
+ "chrome,modal,titlebar",
+ editorElement
+ );
+ },
+};
+
+var nsFindCommand = {
+ isCommandEnabled(aCommand, editorElement) {
+ return editorElement.getEditor(editorElement.contentWindow) != null;
+ },
+
+ getCommandStateParams(aCommand, aParams, editorElement) {},
+ doCommandParams(aCommand, aParams, editorElement) {},
+
+ doCommand(aCommand, editorElement) {
+ document.getElementById("FindToolbar").onFindCommand();
+ },
+};
+
+var nsFindAgainCommand = {
+ isCommandEnabled(aCommand, editorElement) {
+ // we can only do this if the search pattern is non-empty. Not sure how
+ // to get that from here
+ return editorElement.getEditor(editorElement.contentWindow) != null;
+ },
+
+ getCommandStateParams(aCommand, aParams, editorElement) {},
+ doCommandParams(aCommand, aParams, editorElement) {},
+
+ doCommand(aCommand, editorElement) {
+ let findPrev = aCommand == "cmd_findPrev";
+ document.getElementById("FindToolbar").onFindAgainCommand(findPrev);
+ },
+};
+
+var nsRewrapCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return (
+ IsDocumentEditable() &&
+ !IsInHTMLSourceMode() &&
+ GetCurrentEditor() instanceof Ci.nsIEditorMailSupport
+ );
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ GetCurrentEditor().QueryInterface(Ci.nsIEditorMailSupport).rewrap(false);
+ },
+};
+
+var nsSpellingCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return (
+ IsDocumentEditable() && !IsInHTMLSourceMode() && IsSpellCheckerInstalled()
+ );
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ window.cancelSendMessage = false;
+ try {
+ var skipBlockQuotes =
+ window.document.documentElement.getAttribute("windowtype") ==
+ "msgcompose";
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdSpellCheck.xhtml",
+ "_blank",
+ "dialog,close,titlebar,modal,resizable",
+ false,
+ skipBlockQuotes,
+ true
+ );
+ } catch (ex) {}
+ },
+};
+
+var nsImageCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdImageProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal"
+ );
+ },
+};
+
+var nsHLineCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ // Inserting an HLine is different in that we don't use properties dialog
+ // unless we are editing an existing line's attributes
+ // We get the last-used attributes from the prefs and insert immediately
+
+ var tagName = "hr";
+ var editor = GetCurrentEditor();
+
+ var hLine;
+ try {
+ hLine = editor.getSelectedElement(tagName);
+ } catch (e) {
+ return;
+ }
+
+ if (hLine) {
+ // We only open the dialog for an existing HRule
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdHLineProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal"
+ );
+ } else {
+ try {
+ hLine = editor.createElementWithDefaults(tagName);
+
+ // We change the default attributes to those saved in the user prefs
+ let align = Services.prefs.getIntPref("editor.hrule.align");
+ if (align == 0) {
+ editor.setAttributeOrEquivalent(hLine, "align", "left", true);
+ } else if (align == 2) {
+ editor.setAttributeOrEquivalent(hLine, "align", "right", true);
+ }
+
+ // Note: Default is center (don't write attribute)
+
+ let width = Services.prefs.getIntPref("editor.hrule.width");
+ if (Services.prefs.getBoolPref("editor.hrule.width_percent")) {
+ width = width + "%";
+ }
+
+ editor.setAttributeOrEquivalent(hLine, "width", width, true);
+
+ let height = Services.prefs.getIntPref("editor.hrule.height");
+ editor.setAttributeOrEquivalent(hLine, "size", String(height), true);
+
+ if (Services.prefs.getBoolPref("editor.hrule.shading")) {
+ hLine.removeAttribute("noshade");
+ } else {
+ hLine.setAttribute("noshade", "noshade");
+ }
+
+ editor.insertElementAtSelection(hLine, true);
+ } catch (e) {}
+ }
+ },
+};
+
+var nsLinkCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ // If selected element is an image, launch that dialog instead
+ // since last tab panel handles link around an image
+ var element = GetObjectForProperties();
+ if (element && element.nodeName.toLowerCase() == "img") {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdImageProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ null,
+ true
+ );
+ } else {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdLinkProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal"
+ );
+ }
+ },
+};
+
+var nsAnchorCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdNamedAnchorProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ ""
+ );
+ },
+};
+
+var nsInsertHTMLWithDialogCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ gMsgCompose.allowRemoteContent = true;
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdInsSrc.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal,resizable",
+ ""
+ );
+ },
+};
+
+var nsInsertMathWithDialogCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdInsertMath.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal,resizable",
+ ""
+ );
+ },
+};
+
+var nsInsertCharsCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ EditorFindOrCreateInsertCharWindow();
+ },
+};
+
+var nsInsertBreakAllCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentEditor().insertHTML("<br clear='all'>");
+ } catch (e) {}
+ },
+};
+
+var nsListPropertiesCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdListProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal"
+ );
+ },
+};
+
+var nsObjectPropertiesCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ var isEnabled = false;
+ if (IsDocumentEditable() && IsEditingRenderedHTML()) {
+ isEnabled =
+ GetObjectForProperties() != null ||
+ GetCurrentEditor().getSelectedElement("href") != null;
+ }
+ return isEnabled;
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ // Launch Object properties for appropriate selected element
+ var element = GetObjectForProperties();
+ if (element) {
+ var name = element.nodeName.toLowerCase();
+ switch (name) {
+ case "img":
+ gMsgCompose.allowRemoteContent = true;
+ goDoCommand("cmd_image");
+ break;
+ case "hr":
+ goDoCommand("cmd_hline");
+ break;
+ case "table":
+ EditorInsertOrEditTable(false);
+ break;
+ case "td":
+ case "th":
+ EditorTableCellProperties();
+ break;
+ case "ol":
+ case "ul":
+ case "dl":
+ case "li":
+ goDoCommand("cmd_listProperties");
+ break;
+ case "a":
+ if (element.name) {
+ goDoCommand("cmd_anchor");
+ } else if (element.href) {
+ goDoCommand("cmd_link");
+ }
+ break;
+ case "math":
+ goDoCommand("cmd_insertMathWithDialog");
+ break;
+ default:
+ doAdvancedProperties(element);
+ break;
+ }
+ } else {
+ // We get a partially-selected link if asked for specifically
+ try {
+ element = GetCurrentEditor().getSelectedElement("href");
+ } catch (e) {}
+ if (element) {
+ goDoCommand("cmd_link");
+ }
+ }
+ },
+};
+
+var nsSetSmiley = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {
+ try {
+ let editor = GetCurrentEditor();
+ let smileyCode = aParams.getStringValue("state_attribute");
+ editor.insertHTML(smileyCode);
+ window.content.focus();
+ } catch (e) {
+ dump("Exception occurred in smiley InsertElementAtSelection\n");
+ }
+ },
+ // This is now deprecated in favor of "doCommandParams"
+ doCommand(aCommand) {},
+};
+
+function doAdvancedProperties(element) {
+ if (element) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdAdvancedEdit.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal,resizable=yes",
+ "",
+ element
+ );
+ }
+}
+
+var nsColorPropertiesCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdColorProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ ""
+ );
+ UpdateDefaultColors();
+ },
+};
+
+var nsIncreaseFontCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ if (!(IsDocumentEditable() && IsEditingRenderedHTML())) {
+ return false;
+ }
+ let setIndex = parseInt(getLegacyFontSize());
+ return setIndex < 6;
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ let setIndex = parseInt(getLegacyFontSize());
+ EditorSetFontSize((setIndex + 1).toString());
+ },
+};
+
+var nsDecreaseFontCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ if (!(IsDocumentEditable() && IsEditingRenderedHTML())) {
+ return false;
+ }
+ let setIndex = parseInt(getLegacyFontSize());
+ return setIndex > 1;
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ let setIndex = parseInt(getLegacyFontSize());
+ EditorSetFontSize((setIndex - 1).toString());
+ },
+};
+
+var nsRemoveNamedAnchorsCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ // We could see if there's any link in selection, but it doesn't seem worth the work!
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ EditorRemoveTextProperty("name", "");
+ window.content.focus();
+ },
+};
+
+var nsInsertOrEditTableCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ if (IsInTableCell()) {
+ EditorTableCellProperties();
+ } else {
+ EditorInsertOrEditTable(true);
+ }
+ },
+};
+
+var nsEditTableCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTable();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ EditorInsertOrEditTable(false);
+ },
+};
+
+var nsSelectTableCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTable();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().selectTable();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsSelectTableRowCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().selectTableRow();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsSelectTableColumnCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().selectTableColumn();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsSelectTableCellCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().selectTableCell();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsSelectAllTableCellsCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTable();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().selectAllTableCells();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsInsertTableCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsDocumentEditable() && IsEditingRenderedHTML();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ EditorInsertTable();
+ },
+};
+
+var nsInsertTableRowAboveCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().insertTableRow(1, false);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsInsertTableRowBelowCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().insertTableRow(1, true);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsInsertTableColumnBeforeCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().insertTableColumn(1, false);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsInsertTableColumnAfterCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().insertTableColumn(1, true);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsInsertTableCellBeforeCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().insertTableCell(1, false);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsInsertTableCellAfterCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().insertTableCell(1, true);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsDeleteTableCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTable();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().deleteTable();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsDeleteTableRowCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ var rows = GetNumberOfContiguousSelectedRows();
+ // Delete at least one row
+ if (rows == 0) {
+ rows = 1;
+ }
+
+ try {
+ var editor = GetCurrentTableEditor();
+ editor.beginTransaction();
+
+ // Loop to delete all blocks of contiguous, selected rows
+ while (rows) {
+ editor.deleteTableRow(rows);
+ rows = GetNumberOfContiguousSelectedRows();
+ }
+ } finally {
+ editor.endTransaction();
+ }
+ window.content.focus();
+ },
+};
+
+var nsDeleteTableColumnCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ var columns = GetNumberOfContiguousSelectedColumns();
+ // Delete at least one column
+ if (columns == 0) {
+ columns = 1;
+ }
+
+ try {
+ var editor = GetCurrentTableEditor();
+ editor.beginTransaction();
+
+ // Loop to delete all blocks of contiguous, selected columns
+ while (columns) {
+ editor.deleteTableColumn(columns);
+ columns = GetNumberOfContiguousSelectedColumns();
+ }
+ } finally {
+ editor.endTransaction();
+ }
+ window.content.focus();
+ },
+};
+
+var nsDeleteTableCellCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().deleteTableCell(1);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsDeleteTableCellContentsCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTableCell();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().deleteTableCellContents();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsJoinTableCellsCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ if (IsDocumentEditable() && IsEditingRenderedHTML()) {
+ try {
+ var editor = GetCurrentTableEditor();
+ var tagNameObj = { value: "" };
+ var countObj = { value: 0 };
+ var cell = editor.getSelectedOrParentTableElement(tagNameObj, countObj);
+
+ // We need a cell and either > 1 selected cell or a cell to the right
+ // (this cell may originate in a row spanned from above current row)
+ // Note that editor returns "td" for "th" also.
+ // (this is a pain! Editor and gecko use lowercase tagNames, JS uses uppercase!)
+ if (cell && tagNameObj.value == "td") {
+ // Selected cells
+ if (countObj.value > 1) {
+ return true;
+ }
+
+ var colSpan = cell.getAttribute("colspan");
+
+ // getAttribute returns string, we need number
+ // no attribute means colspan = 1
+ if (!colSpan) {
+ colSpan = Number(1);
+ } else {
+ colSpan = Number(colSpan);
+ }
+
+ var rowObj = { value: 0 };
+ var colObj = { value: 0 };
+ editor.getCellIndexes(cell, rowObj, colObj);
+
+ // Test if cell exists to the right of current cell
+ // (cells with 0 span should never have cells to the right
+ // if there is, user can select the 2 cells to join them)
+ return (
+ colSpan &&
+ editor.getCellAt(null, rowObj.value, colObj.value + colSpan)
+ );
+ }
+ } catch (e) {}
+ }
+ return false;
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ // Param: Don't merge non-contiguous cells
+ try {
+ GetCurrentTableEditor().joinTableCells(false);
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsSplitTableCellCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ if (IsDocumentEditable() && IsEditingRenderedHTML()) {
+ var tagNameObj = { value: "" };
+ var countObj = { value: 0 };
+ var cell;
+ try {
+ cell = GetCurrentTableEditor().getSelectedOrParentTableElement(
+ tagNameObj,
+ countObj
+ );
+ } catch (e) {}
+
+ // We need a cell parent and there's just 1 selected cell
+ // or selection is entirely inside 1 cell
+ if (
+ cell &&
+ tagNameObj.value == "td" &&
+ countObj.value <= 1 &&
+ IsSelectionInOneCell()
+ ) {
+ var colSpan = cell.getAttribute("colspan");
+ var rowSpan = cell.getAttribute("rowspan");
+ if (!colSpan) {
+ colSpan = 1;
+ }
+ if (!rowSpan) {
+ rowSpan = 1;
+ }
+ return colSpan > 1 || rowSpan > 1 || colSpan == 0 || rowSpan == 0;
+ }
+ }
+ return false;
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ try {
+ GetCurrentTableEditor().splitTableCell();
+ } catch (e) {}
+ window.content.focus();
+ },
+};
+
+var nsTableOrCellColorCommand = {
+ isCommandEnabled(aCommand, dummy) {
+ return IsInTable();
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ EditorSelectColor("TableOrCell");
+ },
+};
+
+var nsConvertToTable = {
+ isCommandEnabled(aCommand, dummy) {
+ if (IsDocumentEditable() && IsEditingRenderedHTML()) {
+ var selection;
+ try {
+ selection = GetCurrentEditor().selection;
+ } catch (e) {}
+
+ if (selection && !selection.isCollapsed) {
+ // Don't allow if table or cell is the selection
+ var element;
+ try {
+ element = GetCurrentEditor().getSelectedElement("");
+ } catch (e) {}
+ if (element) {
+ var name = element.nodeName.toLowerCase();
+ if (
+ name == "td" ||
+ name == "th" ||
+ name == "caption" ||
+ name == "table"
+ ) {
+ return false;
+ }
+ }
+
+ // Selection start and end must be in the same cell
+ // in same cell or both are NOT in a cell
+ if (
+ GetParentTableCell(selection.focusNode) !=
+ GetParentTableCell(selection.anchorNode)
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+ return false;
+ },
+
+ getCommandStateParams(aCommand, aParams, aRefCon) {},
+ doCommandParams(aCommand, aParams, aRefCon) {},
+
+ doCommand(aCommand) {
+ if (this.isCommandEnabled()) {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdConvertToTable.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal"
+ );
+ }
+ },
+};
diff --git a/comm/mail/components/compose/content/MsgComposeCommands.js b/comm/mail/components/compose/content/MsgComposeCommands.js
new file mode 100644
index 0000000000..6a0045b58d
--- /dev/null
+++ b/comm/mail/components/compose/content/MsgComposeCommands.js
@@ -0,0 +1,11654 @@
+/* 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 ../../../../../toolkit/content/contentAreaUtils.js */
+/* import-globals-from ../../../../mailnews/addrbook/content/abDragDrop.js */
+/* import-globals-from ../../../../mailnews/base/prefs/content/accountUtils.js */
+/* import-globals-from ../../../base/content/contentAreaClick.js */
+/* import-globals-from ../../../base/content/mailCore.js */
+/* import-globals-from ../../../base/content/messenger-customization.js */
+/* import-globals-from ../../../base/content/toolbarIconColor.js */
+/* import-globals-from ../../../base/content/utilityOverlay.js */
+/* import-globals-from ../../../base/content/viewZoomOverlay.js */
+/* import-globals-from ../../../base/content/widgets/browserPopups.js */
+/* import-globals-from ../../../extensions/openpgp/content/ui/keyAssistant.js */
+/* import-globals-from addressingWidgetOverlay.js */
+/* import-globals-from cloudAttachmentLinkManager.js */
+/* import-globals-from ComposerCommands.js */
+/* import-globals-from editor.js */
+/* import-globals-from editorUtilities.js */
+
+/**
+ * Commands for the message composition window.
+ */
+
+// Ensure the activity modules are loaded for this window.
+ChromeUtils.import("resource:///modules/activity/activityModules.jsm");
+var { AttachmentChecker } = ChromeUtils.import(
+ "resource:///modules/AttachmentChecker.jsm"
+);
+var { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ SelectionUtils: "resource://gre/modules/SelectionUtils.sys.mjs",
+ ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FolderUtils: "resource:///modules/FolderUtils.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ BondOpenPGP: "chrome://openpgp/content/BondOpenPGP.jsm",
+ UIFontSize: "resource:///modules/UIFontSize.jsm",
+ UIDensity: "resource:///modules/UIDensity.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ this,
+ "l10nCompose",
+ () =>
+ new Localization([
+ "branding/brand.ftl",
+ "messenger/messengercompose/messengercompose.ftl",
+ ])
+);
+
+XPCOMUtils.defineLazyGetter(
+ this,
+ "l10nComposeSync",
+ () =>
+ new Localization(
+ ["branding/brand.ftl", "messenger/messengercompose/messengercompose.ftl"],
+ true
+ )
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "PrintUtils",
+ "chrome://messenger/content/printUtils.js"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+});
+
+/**
+ * Global message window object. This is used by mail-offline.js and therefore
+ * should not be renamed. We need to avoid doing this kind of cross file global
+ * stuff in the future and instead pass this object as parameter when needed by
+ * functions in the other js file.
+ */
+var msgWindow;
+
+var gMessenger;
+
+/**
+ * Global variables, need to be re-initialized every time mostly because
+ * we need to release them when the window closes.
+ */
+var gMsgCompose;
+var gOriginalMsgURI;
+var gWindowLocked;
+var gSendLocked;
+var gContentChanged;
+var gSubjectChanged;
+var gAutoSaving;
+var gCurrentIdentity;
+var defaultSaveOperation;
+var gSendOperationInProgress;
+var gSaveOperationInProgress;
+var gCloseWindowAfterSave;
+var gSavedSendNowKey;
+var gContextMenu;
+var gLastFocusElement = null;
+var gLoadingComplete = false;
+
+var gAttachmentBucket;
+var gAttachmentCounter;
+/**
+ * typedef {Object} FocusArea
+ *
+ * @property {Element} root - The root of a given area of the UI.
+ * @property {moveFocusWithin} focus - A method to move the focus within the
+ * root.
+ */
+/**
+ * @callback moveFocusWithin
+ *
+ * @param {Element} root - The element to move the focus within.
+ *
+ * @returns {boolean} - Whether the focus was successfully moved to within the
+ * given element.
+ */
+/**
+ * An ordered list of non-intersecting areas we want to jump focus between.
+ * Ordering should be in the same order as tab focus. See
+ * {@link moveFocusToNeighbouringArea}.
+ *
+ * @type {FocusArea[]}
+ */
+var gFocusAreas;
+// TODO: Maybe the following two variables can be combined.
+var gManualAttachmentReminder;
+var gDisableAttachmentReminder;
+var gComposeType;
+var gLanguageObserver;
+var gRecipientObserver;
+var gWantCannotEncryptBCCNotification = true;
+var gRecipientKeysObserver;
+var gCheckPublicRecipientsTimer;
+var gBodyFromArgs;
+
+// gSMFields is the nsIMsgComposeSecure instance for S/MIME.
+// gMsgCompose.compFields.composeSecure is set to this instance most of
+// the time. Because the S/MIME code has no knowledge of the OpenPGP
+// implementation, gMsgCompose.compFields.composeSecure is set to an
+// instance of PgpMimeEncrypt only temporarily. Keeping variable
+// gSMFields separate allows switching as needed.
+var gSMFields = null;
+
+var gSMPendingCertLookupSet = new Set();
+var gSMCertsAlreadyLookedUpInLDAP = new Set();
+
+var gSelectedTechnologyIsPGP = false;
+
+// The initial flags store the value we used at composer open time.
+// Some flags might be automatically changed as a consequence of other
+// changes. When reverting automatic actions, the initial flags help
+// us know what value we should use for restoring.
+
+var gSendSigned = false;
+
+var gAttachMyPublicPGPKey = false;
+
+var gSendEncrypted = false;
+
+// gEncryptSubject contains the preference for subject encryption,
+// considered only if encryption is enabled and the technology allows it.
+// In other words, gEncryptSubject might be set to true, but if
+// encryption is disabled, or if S/MIME is used,
+// gEncryptSubject==true is ignored.
+var gEncryptSubject = false;
+
+var gUserTouchedSendEncrypted = false;
+var gUserTouchedSendSigned = false;
+var gUserTouchedAttachMyPubKey = false;
+var gUserTouchedEncryptSubject = false;
+
+var gIsRelatedToEncryptedOriginal = false;
+
+var gOpened = Date.now();
+
+var gEncryptedURIService = Cc[
+ "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"
+].getService(Ci.nsIEncryptedSMIMEURIsService);
+
+try {
+ var gDragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+ );
+} catch (e) {}
+
+/**
+ * Boolean variable to keep track of the dragging action of files above the
+ * compose window.
+ *
+ * @type {boolean}
+ */
+var gIsDraggingAttachments;
+
+/**
+ * Boolean variable to allow showing the attach inline overlay when dragging
+ * links that otherwise would only trigger the add as attachment overlay.
+ *
+ * @type {boolean}
+ */
+var gIsValidInline;
+
+// i18n globals
+var _gComposeBundle;
+function getComposeBundle() {
+ // That one has to be lazy. Getting a reference to an element with a XBL
+ // binding attached will cause the XBL constructors to fire if they haven't
+ // already. If we get a reference to the compose bundle at script load-time,
+ // this will cause the XBL constructor that's responsible for the personas to
+ // fire up, thus executing the personas code while the DOM is not fully built.
+ // Since this <script> comes before the <statusbar>, the Personas code will
+ // fail.
+ if (!_gComposeBundle) {
+ _gComposeBundle = document.getElementById("bundle_composeMsgs");
+ }
+ return _gComposeBundle;
+}
+
+var gLastWindowToHaveFocus;
+var gLastKnownComposeStates;
+var gReceiptOptionChanged;
+var gDSNOptionChanged;
+var gAttachVCardOptionChanged;
+
+var gAutoSaveInterval;
+var gAutoSaveTimeout;
+var gAutoSaveKickedIn;
+var gEditingDraft;
+var gNumUploadingAttachments;
+
+// From the user's point-of-view, is spell checking enabled? This value only
+// changes if the user makes the change, it's not affected by the process of
+// sending or saving the message or any other reason the actual state of the
+// spellchecker might change.
+var gSpellCheckingEnabled;
+
+var kComposeAttachDirPrefName = "mail.compose.attach.dir";
+
+window.addEventListener("unload", event => {
+ ComposeUnload();
+});
+window.addEventListener("load", event => {
+ ComposeLoad();
+});
+window.addEventListener("close", event => {
+ if (!ComposeCanClose()) {
+ event.preventDefault();
+ }
+});
+window.addEventListener("focus", event => {
+ EditorOnFocus();
+});
+window.addEventListener("click", event => {
+ composeWindowOnClick(event);
+});
+
+document.addEventListener("focusin", event => {
+ // Listen for focusin event in composition. gLastFocusElement might well be
+ // null, e.g. when focusin enters a different document like contacts sidebar.
+ gLastFocusElement = event.relatedTarget;
+});
+
+// For WebExtensions.
+this.__defineGetter__("browser", GetCurrentEditorElement);
+
+/**
+ * @implements {nsIXULBrowserWindow}
+ */
+var XULBrowserWindow = {
+ // Used to show the link-being-hovered-over in the status bar. Do nothing here.
+ setOverLink(url, anchorElt) {},
+
+ // Called before links are navigated to to allow us to retarget them if needed.
+ onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
+ return originalTarget;
+ },
+
+ // Called by BrowserParent::RecvShowTooltip.
+ showTooltip(xDevPix, yDevPix, tooltip, direction, browser) {
+ if (
+ Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService)
+ .getCurrentSession()
+ ) {
+ return;
+ }
+
+ let elt = document.getElementById("remoteBrowserTooltip");
+ elt.label = tooltip;
+ elt.style.direction = direction;
+ elt.openPopupAtScreen(
+ xDevPix / window.devicePixelRatio,
+ yDevPix / window.devicePixelRatio,
+ false,
+ null
+ );
+ },
+
+ // Called by BrowserParent::RecvHideTooltip.
+ hideTooltip() {
+ let elt = document.getElementById("remoteBrowserTooltip");
+ elt.hidePopup();
+ },
+
+ getTabCount() {
+ return 1;
+ },
+};
+window
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).XULBrowserWindow = window.XULBrowserWindow;
+
+// Observer for the autocomplete input.
+const inputObserver = {
+ observe: (subject, topic, data) => {
+ if (topic == "autocomplete-did-enter-text") {
+ let input = subject.QueryInterface(
+ Ci.nsIAutoCompleteInput
+ ).wrappedJSObject;
+
+ // Interrupt if there's no input proxy, or the input doesn't have an ID,
+ // the latter meaning that the autocomplete event was triggered within an
+ // already existing pill, so we don't want to create a new pill.
+ if (!input || !input.id) {
+ return;
+ }
+
+ // Trigger the pill creation.
+ recipientAddPills(document.getElementById(input.id));
+ }
+ },
+};
+
+const keyObserver = {
+ observe: async (subject, topic, data) => {
+ switch (topic) {
+ case "openpgp-key-change":
+ EnigmailKeyRing.clearCache();
+ // fall through
+ case "openpgp-acceptance-change":
+ checkEncryptionState(topic);
+ gKeyAssistant.onExternalKeyChange();
+ break;
+ default:
+ break;
+ }
+ },
+};
+
+// Non translatable international shortcuts.
+var SHOW_TO_KEY = "T";
+var SHOW_CC_KEY = "C";
+var SHOW_BCC_KEY = "B";
+
+function InitializeGlobalVariables() {
+ gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+ gMsgCompose = null;
+ gOriginalMsgURI = null;
+ gWindowLocked = false;
+ gContentChanged = false;
+ gSubjectChanged = false;
+ gCurrentIdentity = null;
+ defaultSaveOperation = "draft";
+ gSendOperationInProgress = false;
+ gSaveOperationInProgress = false;
+ gAutoSaving = false;
+ gCloseWindowAfterSave = false;
+ gSavedSendNowKey = null;
+ gManualAttachmentReminder = false;
+ gDisableAttachmentReminder = false;
+ gLanguageObserver = null;
+ gRecipientObserver = null;
+
+ gLastWindowToHaveFocus = null;
+ gLastKnownComposeStates = {};
+ gReceiptOptionChanged = false;
+ gDSNOptionChanged = false;
+ gAttachVCardOptionChanged = false;
+ gNumUploadingAttachments = 0;
+ // eslint-disable-next-line no-global-assign
+ msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+ );
+ MailServices.mailSession.AddMsgWindow(msgWindow);
+
+ // Add the observer.
+ Services.obs.addObserver(inputObserver, "autocomplete-did-enter-text");
+ Services.obs.addObserver(keyObserver, "openpgp-key-change");
+ Services.obs.addObserver(keyObserver, "openpgp-acceptance-change");
+}
+InitializeGlobalVariables();
+
+function ReleaseGlobalVariables() {
+ gCurrentIdentity = null;
+ gMsgCompose = null;
+ gOriginalMsgURI = null;
+ gMessenger = null;
+ gRecipientObserver = null;
+ gDisableAttachmentReminder = false;
+ _gComposeBundle = null;
+ MailServices.mailSession.RemoveMsgWindow(msgWindow);
+ // eslint-disable-next-line no-global-assign
+ msgWindow = null;
+
+ gLastKnownComposeStates = null;
+
+ // Remove the observers.
+ Services.obs.removeObserver(inputObserver, "autocomplete-did-enter-text");
+ Services.obs.removeObserver(keyObserver, "openpgp-key-change");
+ Services.obs.removeObserver(keyObserver, "openpgp-acceptance-change");
+}
+
+// Notification box shown at the bottom of the window.
+XPCOMUtils.defineLazyGetter(this, "gComposeNotification", () => {
+ return new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "bottom");
+ document.getElementById("compose-notification-bottom").append(element);
+ });
+});
+
+/**
+ * Get the first next sibling element matching the selector (if specified).
+ *
+ * @param {HTMLElement} element - The source element whose sibling to look for.
+ * @param {string} [selector] - The CSS query selector to match.
+ *
+ * @returns {(HTMLElement|null)} - The first matching sibling element, or null.
+ */
+function getNextSibling(element, selector) {
+ let sibling = element.nextElementSibling;
+ if (!selector) {
+ // If there's no selector, return the first next sibling.
+ return sibling;
+ }
+ while (sibling) {
+ if (sibling.matches(selector)) {
+ // Return the current sibling if it matches the selector.
+ return sibling;
+ }
+ // Otherwise, continue the loop with the following next sibling.
+ sibling = sibling.nextElementSibling;
+ }
+ return null;
+}
+
+/**
+ * Get the first previous sibling element matching the selector (if specified).
+ *
+ * @param {HTMLElement} element - The source element whose sibling to look for.
+ * @param {string} [selector] - The CSS query selector to match.
+ *
+ * @returns {(HTMLElement|null)} - The first matching sibling element, or null.
+ */
+function getPreviousSibling(element, selector) {
+ let sibling = element.previousElementSibling;
+ if (!selector) {
+ // If there's no selector, return the first previous sibling.
+ return sibling;
+ }
+ while (sibling) {
+ if (sibling.matches(selector)) {
+ // Return the current sibling if it matches the selector.
+ return sibling;
+ }
+ // Otherwise, continue the loop with the preceding previous sibling.
+ sibling = sibling.previousElementSibling;
+ }
+ return null;
+}
+
+/**
+ * Get a pretty, human-readable shortcut key string from a given <key> id.
+ *
+ * @param aKeyId the ID of a <key> element
+ * @returns string pretty, human-readable shortcut key string from the <key>
+ */
+function getPrettyKey(aKeyId) {
+ return ShortcutUtils.prettifyShortcut(document.getElementById(aKeyId));
+}
+
+/**
+ * Disables or enables editable elements in the window.
+ * The elements to operate on are marked with the "disableonsend" attribute.
+ * This includes elements like the address list, attachment list, subject
+ * and message body.
+ *
+ * @param aDisable true = disable items. false = enable items.
+ */
+function updateEditableFields(aDisable) {
+ if (!gMsgCompose) {
+ return;
+ }
+
+ if (aDisable) {
+ gMsgCompose.editor.flags |= Ci.nsIEditor.eEditorReadonlyMask;
+ } else {
+ gMsgCompose.editor.flags &= ~Ci.nsIEditor.eEditorReadonlyMask;
+
+ try {
+ let checker = GetCurrentEditor().getInlineSpellChecker(true);
+ checker.enableRealTimeSpell = gSpellCheckingEnabled;
+ } catch (ex) {
+ // An error will be thrown if there are no dictionaries. Just ignore it.
+ }
+ }
+
+ // Disable all the input fields and labels.
+ for (let element of document.querySelectorAll('[disableonsend="true"]')) {
+ element.disabled = aDisable;
+ }
+
+ // Update the UI of the addressing rows.
+ for (let row of document.querySelectorAll(".address-container")) {
+ row.classList.toggle("disable-container", aDisable);
+ }
+
+ // Prevent any interaction with the addressing pills.
+ for (let pill of document.querySelectorAll("mail-address-pill")) {
+ pill.toggleAttribute("disabled", aDisable);
+ }
+}
+
+/**
+ * Small helper function to check whether the node passed in is a signature.
+ * Note that a text node is not a DOM element, hence .localName can't be used.
+ */
+function isSignature(aNode) {
+ return (
+ ["DIV", "PRE"].includes(aNode.nodeName) &&
+ aNode.classList.contains("moz-signature")
+ );
+}
+
+var stateListener = {
+ NotifyComposeFieldsReady() {
+ ComposeFieldsReady();
+ updateSendCommands(true);
+ },
+
+ NotifyComposeBodyReady() {
+ // Look all the possible compose types (nsIMsgComposeParams.idl):
+ switch (gComposeType) {
+ case Ci.nsIMsgCompType.MailToUrl:
+ gBodyFromArgs = true;
+ // Falls through
+ case Ci.nsIMsgCompType.New:
+ case Ci.nsIMsgCompType.NewsPost:
+ case Ci.nsIMsgCompType.ForwardAsAttachment:
+ this.NotifyComposeBodyReadyNew();
+ break;
+
+ case Ci.nsIMsgCompType.Reply:
+ case Ci.nsIMsgCompType.ReplyAll:
+ case Ci.nsIMsgCompType.ReplyToSender:
+ case Ci.nsIMsgCompType.ReplyToGroup:
+ case Ci.nsIMsgCompType.ReplyToSenderAndGroup:
+ case Ci.nsIMsgCompType.ReplyWithTemplate:
+ case Ci.nsIMsgCompType.ReplyToList:
+ this.NotifyComposeBodyReadyReply();
+ break;
+
+ case Ci.nsIMsgCompType.Redirect:
+ case Ci.nsIMsgCompType.ForwardInline:
+ this.NotifyComposeBodyReadyForwardInline();
+ break;
+
+ case Ci.nsIMsgCompType.EditTemplate:
+ defaultSaveOperation = "template";
+ break;
+ case Ci.nsIMsgCompType.Draft:
+ case Ci.nsIMsgCompType.Template:
+ case Ci.nsIMsgCompType.EditAsNew:
+ break;
+
+ default:
+ dump(
+ "Unexpected nsIMsgCompType in NotifyComposeBodyReady (" +
+ gComposeType +
+ ")\n"
+ );
+ }
+
+ // Setting the selected item in the identity list will cause an
+ // identity/signature switch. This can only be done once the message
+ // body has already been assembled with the signature we need to switch.
+ if (gMsgCompose.identity != gCurrentIdentity) {
+ let identityList = document.getElementById("msgIdentity");
+ identityList.selectedItem = identityList.getElementsByAttribute(
+ "identitykey",
+ gMsgCompose.identity.key
+ )[0];
+ LoadIdentity(false);
+ }
+ if (gMsgCompose.composeHTML) {
+ loadHTMLMsgPrefs();
+ }
+ AdjustFocus();
+ },
+
+ NotifyComposeBodyReadyNew() {
+ let useParagraph = Services.prefs.getBoolPref(
+ "mail.compose.default_to_paragraph"
+ );
+ let insertParagraph = gMsgCompose.composeHTML && useParagraph;
+
+ let mailBody = getBrowser().contentDocument.querySelector("body");
+ if (insertParagraph && gBodyFromArgs) {
+ // Check for "empty" body before allowing paragraph to be inserted.
+ // Non-empty bodies in a new message can occur when clicking on a
+ // mailto link or when using the command line option -compose.
+ // An "empty" body can be one of these three cases:
+ // 1) <br> and nothing follows (no next sibling)
+ // 2) <div/pre class="moz-signature">
+ // 3) No elements, just text
+ // Note that <br><div/pre class="moz-signature"> doesn't happen in
+ // paragraph mode.
+ let firstChild = mailBody.firstChild;
+ let firstElementChild = mailBody.firstElementChild;
+ if (firstElementChild) {
+ if (
+ (firstElementChild.nodeName != "BR" ||
+ firstElementChild.nextElementSibling) &&
+ !isSignature(firstElementChild)
+ ) {
+ insertParagraph = false;
+ }
+ } else if (firstChild && firstChild.nodeType == Node.TEXT_NODE) {
+ insertParagraph = false;
+ }
+ }
+
+ // Control insertion of line breaks.
+ if (insertParagraph) {
+ let editor = GetCurrentEditor();
+ editor.enableUndo(false);
+
+ editor.selection.collapse(mailBody, 0);
+ let pElement = editor.createElementWithDefaults("p");
+ pElement.appendChild(editor.createElementWithDefaults("br"));
+ editor.insertElementAtSelection(pElement, false);
+
+ document.getElementById("cmd_paragraphState").setAttribute("state", "p");
+
+ editor.beginningOfDocument();
+ editor.enableUndo(true);
+ editor.resetModificationCount();
+ } else {
+ document.getElementById("cmd_paragraphState").setAttribute("state", "");
+ }
+ onParagraphFormatChange();
+ },
+
+ NotifyComposeBodyReadyReply() {
+ // Control insertion of line breaks.
+ let useParagraph = Services.prefs.getBoolPref(
+ "mail.compose.default_to_paragraph"
+ );
+ if (gMsgCompose.composeHTML && useParagraph) {
+ let mailBody = getBrowser().contentDocument.querySelector("body");
+ let editor = GetCurrentEditor();
+ let selection = editor.selection;
+
+ // Make sure the selection isn't inside the signature.
+ if (isSignature(mailBody.firstElementChild)) {
+ selection.collapse(mailBody, 0);
+ }
+
+ let range = selection.getRangeAt(0);
+ let start = range.startOffset;
+
+ if (start != range.endOffset) {
+ // The selection is not collapsed, most likely due to the
+ // "select the quote" option. In this case we do nothing.
+ return;
+ }
+
+ if (range.startContainer != mailBody) {
+ dump("Unexpected selection in NotifyComposeBodyReadyReply\n");
+ return;
+ }
+
+ editor.enableUndo(false);
+
+ let pElement = editor.createElementWithDefaults("p");
+ pElement.appendChild(editor.createElementWithDefaults("br"));
+ editor.insertElementAtSelection(pElement, false);
+
+ // Position into the paragraph.
+ selection.collapse(pElement, 0);
+
+ document.getElementById("cmd_paragraphState").setAttribute("state", "p");
+
+ editor.enableUndo(true);
+ editor.resetModificationCount();
+ } else {
+ document.getElementById("cmd_paragraphState").setAttribute("state", "");
+ }
+ onParagraphFormatChange();
+ },
+
+ NotifyComposeBodyReadyForwardInline() {
+ let mailBody = getBrowser().contentDocument.querySelector("body");
+ let editor = GetCurrentEditor();
+ let selection = editor.selection;
+
+ editor.enableUndo(false);
+
+ // Control insertion of line breaks.
+ selection.collapse(mailBody, 0);
+ let useParagraph = Services.prefs.getBoolPref(
+ "mail.compose.default_to_paragraph"
+ );
+ if (gMsgCompose.composeHTML && useParagraph) {
+ let pElement = editor.createElementWithDefaults("p");
+ let brElement = editor.createElementWithDefaults("br");
+ pElement.appendChild(brElement);
+ editor.insertElementAtSelection(pElement, false);
+ document.getElementById("cmd_paragraphState").setAttribute("state", "p");
+ } else {
+ // insertLineBreak() has been observed to insert two <br> elements
+ // instead of one before a <div>, so we'll do it ourselves here.
+ let brElement = editor.createElementWithDefaults("br");
+ editor.insertElementAtSelection(brElement, false);
+ document.getElementById("cmd_paragraphState").setAttribute("state", "");
+ }
+
+ onParagraphFormatChange();
+ editor.beginningOfDocument();
+ editor.enableUndo(true);
+ editor.resetModificationCount();
+ },
+
+ ComposeProcessDone(aResult) {
+ ToggleWindowLock(false);
+
+ if (aResult == Cr.NS_OK) {
+ if (!gAutoSaving) {
+ SetContentAndBodyAsUnmodified();
+ }
+
+ if (gCloseWindowAfterSave) {
+ // Notify the SendListener that Send has been aborted and Stopped
+ if (gMsgCompose) {
+ gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT);
+ }
+
+ MsgComposeCloseWindow();
+ }
+ } else if (gAutoSaving) {
+ // If we failed to save, and we're autosaving, need to re-mark the editor
+ // as changed, so that we won't lose the changes.
+ gMsgCompose.bodyModified = true;
+ gContentChanged = true;
+ }
+ gAutoSaving = false;
+ gCloseWindowAfterSave = false;
+ },
+
+ SaveInFolderDone(folderURI) {
+ DisplaySaveFolderDlg(folderURI);
+ },
+};
+
+var gSendListener = {
+ // nsIMsgSendListener
+ onStartSending(aMsgID, aMsgSize) {},
+ onProgress(aMsgID, aProgress, aProgressMax) {},
+ onStatus(aMsgID, aMsg) {},
+ onStopSending(aMsgID, aStatus, aMsg, aReturnFile) {
+ if (Components.isSuccessCode(aStatus)) {
+ Services.obs.notifyObservers(null, "mail:composeSendSucceeded", aMsgID);
+ }
+ },
+ onGetDraftFolderURI(aMsgID, aFolderURI) {},
+ onSendNotPerformed(aMsgID, aStatus) {},
+ onTransportSecurityError(msgID, status, secInfo, location) {
+ // We're only interested in Bad Cert errors here.
+ let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
+ Ci.nsINSSErrorsService
+ );
+ let errorClass = nssErrorsService.getErrorClass(status);
+ if (errorClass != Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ return;
+ }
+
+ // Give the user the option of adding an exception for the bad cert.
+ let params = {
+ exceptionAdded: false,
+ securityInfo: secInfo,
+ prefetchCert: true,
+ location,
+ };
+ window.openDialog(
+ "chrome://pippki/content/exceptionDialog.xhtml",
+ "",
+ "chrome,centerscreen,modal",
+ params
+ );
+ // params.exceptionAdded will be set if the user added an exception.
+ },
+};
+
+// all progress notifications are done through the nsIWebProgressListener implementation...
+var progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ let progressMeter = document.getElementById("compose-progressmeter");
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ progressMeter.hidden = false;
+ progressMeter.removeAttribute("value");
+ }
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ gSendOperationInProgress = false;
+ gSaveOperationInProgress = false;
+ progressMeter.hidden = true;
+ progressMeter.value = 0;
+ document.getElementById("statusText").textContent = "";
+ Services.obs.notifyObservers(
+ { composeWindow: window },
+ "mail:composeSendProgressStop"
+ );
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ // Calculate percentage.
+ var percent;
+ if (aMaxTotalProgress > 0) {
+ percent = Math.round((aCurTotalProgress * 100) / aMaxTotalProgress);
+ if (percent > 100) {
+ percent = 100;
+ }
+
+ // Advance progress meter.
+ document.getElementById("compose-progressmeter").value = percent;
+ } else {
+ // Progress meter should be barber-pole in this case.
+ document.getElementById("compose-progressmeter").removeAttribute("value");
+ }
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // we can ignore this notification
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ // Looks like it's possible that we get call while the document has been already delete!
+ // therefore we need to protect ourself by using try/catch
+ try {
+ let statusText = document.getElementById("statusText");
+ if (statusText) {
+ statusText.textContent = aMessage;
+ }
+ } catch (ex) {}
+ },
+
+ onSecurityChange(aWebProgress, aRequest, state) {
+ // we can ignore this notification
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
+ // we can ignore this notification
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+var defaultController = {
+ commands: {
+ cmd_attachFile: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ AttachFile();
+ },
+ },
+
+ cmd_attachCloud: {
+ isEnabled() {
+ // Hide the command entirely if there are no cloud accounts or
+ // the feature is disabled.
+ let cmd = document.getElementById("cmd_attachCloud");
+ cmd.hidden =
+ !Services.prefs.getBoolPref("mail.cloud_files.enabled") ||
+ cloudFileAccounts.configuredAccounts.length == 0 ||
+ Services.io.offline;
+ return !cmd.hidden && !gWindowLocked;
+ },
+ doCommand() {
+ // We should never actually call this, since the <command> node calls
+ // a different function.
+ },
+ },
+
+ cmd_attachPage: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ gMsgCompose.allowRemoteContent = true;
+ AttachPage();
+ },
+ },
+
+ cmd_attachVCard: {
+ isEnabled() {
+ let cmd = document.getElementById("cmd_attachVCard");
+ cmd.setAttribute("checked", gMsgCompose.compFields.attachVCard);
+ return !!gCurrentIdentity?.escapedVCard;
+ },
+ doCommand() {},
+ },
+
+ cmd_attachPublicKey: {
+ isEnabled() {
+ let cmd = document.getElementById("cmd_attachPublicKey");
+ cmd.setAttribute("checked", gAttachMyPublicPGPKey);
+ return isPgpConfigured();
+ },
+ doCommand() {},
+ },
+
+ cmd_toggleAttachmentPane: {
+ isEnabled() {
+ return !gWindowLocked && gAttachmentBucket.itemCount;
+ },
+ doCommand() {
+ toggleAttachmentPane("toggle");
+ },
+ },
+
+ cmd_reorderAttachments: {
+ isEnabled() {
+ if (!gAttachmentBucket.itemCount) {
+ let reorderAttachmentsPanel = document.getElementById(
+ "reorderAttachmentsPanel"
+ );
+ if (reorderAttachmentsPanel.state == "open") {
+ // When the panel is open and all attachments get deleted,
+ // we get notified here and want to close the panel.
+ reorderAttachmentsPanel.hidePopup();
+ }
+ }
+ return gAttachmentBucket.itemCount > 1;
+ },
+ doCommand() {
+ showReorderAttachmentsPanel();
+ },
+ },
+
+ cmd_removeAllAttachments: {
+ isEnabled() {
+ return !gWindowLocked && gAttachmentBucket.itemCount;
+ },
+ doCommand() {
+ RemoveAllAttachments();
+ },
+ },
+
+ cmd_close: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ if (ComposeCanClose()) {
+ window.close();
+ }
+ },
+ },
+
+ cmd_saveDefault: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ Save();
+ },
+ },
+
+ cmd_saveAsFile: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ SaveAsFile(true);
+ },
+ },
+
+ cmd_saveAsDraft: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ SaveAsDraft();
+ },
+ },
+
+ cmd_saveAsTemplate: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ SaveAsTemplate();
+ },
+ },
+
+ cmd_sendButton: {
+ isEnabled() {
+ return !gWindowLocked && !gNumUploadingAttachments && !gSendLocked;
+ },
+ doCommand() {
+ if (Services.io.offline) {
+ SendMessageLater();
+ } else {
+ SendMessage();
+ }
+ },
+ },
+
+ cmd_sendNow: {
+ isEnabled() {
+ return (
+ !gWindowLocked &&
+ !Services.io.offline &&
+ !gSendLocked &&
+ !gNumUploadingAttachments
+ );
+ },
+ doCommand() {
+ SendMessage();
+ },
+ },
+
+ cmd_sendLater: {
+ isEnabled() {
+ return !gWindowLocked && !gNumUploadingAttachments && !gSendLocked;
+ },
+ doCommand() {
+ SendMessageLater();
+ },
+ },
+
+ cmd_sendWithCheck: {
+ isEnabled() {
+ return !gWindowLocked && !gNumUploadingAttachments && !gSendLocked;
+ },
+ doCommand() {
+ SendMessageWithCheck();
+ },
+ },
+
+ cmd_print: {
+ isEnabled() {
+ return !gWindowLocked;
+ },
+ doCommand() {
+ DoCommandPrint();
+ },
+ },
+
+ cmd_delete: {
+ isEnabled() {
+ let cmdDelete = document.getElementById("cmd_delete");
+ let textValue = cmdDelete.getAttribute("valueDefault");
+ let accesskeyValue = cmdDelete.getAttribute("valueDefaultAccessKey");
+
+ cmdDelete.setAttribute("label", textValue);
+ cmdDelete.setAttribute("accesskey", accesskeyValue);
+
+ return false;
+ },
+ doCommand() {},
+ },
+
+ cmd_account: {
+ isEnabled() {
+ return true;
+ },
+ doCommand() {
+ let currentAccountKey = getCurrentAccountKey();
+ let account = MailServices.accounts.getAccount(currentAccountKey);
+ MsgAccountManager(null, account.incomingServer);
+ },
+ },
+
+ cmd_showFormatToolbar: {
+ isEnabled() {
+ return gMsgCompose && gMsgCompose.composeHTML;
+ },
+ doCommand() {
+ goToggleToolbar("FormatToolbar", "menu_showFormatToolbar");
+ },
+ },
+
+ cmd_quoteMessage: {
+ isEnabled() {
+ let selectedURIs = GetSelectedMessages();
+ return selectedURIs && selectedURIs.length > 0;
+ },
+ doCommand() {
+ QuoteSelectedMessage();
+ },
+ },
+
+ cmd_toggleReturnReceipt: {
+ isEnabled() {
+ if (!gMsgCompose) {
+ return false;
+ }
+ return !gWindowLocked;
+ },
+ doCommand() {
+ ToggleReturnReceipt();
+ },
+ },
+
+ cmd_fullZoomReduce: {
+ isEnabled() {
+ return true;
+ },
+ doCommand() {
+ ZoomManager.reduce();
+ },
+ },
+
+ cmd_fullZoomEnlarge: {
+ isEnabled() {
+ return true;
+ },
+ doCommand() {
+ ZoomManager.enlarge();
+ },
+ },
+
+ cmd_fullZoomReset: {
+ isEnabled() {
+ return true;
+ },
+ doCommand() {
+ ZoomManager.reset();
+ },
+ },
+
+ cmd_spelling: {
+ isEnabled() {
+ return true;
+ },
+ doCommand() {
+ window.cancelSendMessage = false;
+ var skipBlockQuotes =
+ window.document.documentElement.getAttribute("windowtype") ==
+ "msgcompose";
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdSpellCheck.xhtml",
+ "_blank",
+ "dialog,close,titlebar,modal,resizable",
+ false,
+ skipBlockQuotes,
+ true
+ );
+ },
+ },
+
+ cmd_fullZoomToggle: {
+ isEnabled() {
+ return true;
+ },
+ doCommand() {
+ ZoomManager.toggleZoom();
+ },
+ },
+ },
+
+ supportsCommand(aCommand) {
+ return aCommand in this.commands;
+ },
+
+ isCommandEnabled(aCommand) {
+ if (!this.supportsCommand(aCommand)) {
+ return false;
+ }
+ return this.commands[aCommand].isEnabled();
+ },
+
+ doCommand(aCommand) {
+ if (!this.supportsCommand(aCommand)) {
+ return;
+ }
+ var cmd = this.commands[aCommand];
+ if (!cmd.isEnabled()) {
+ return;
+ }
+ cmd.doCommand();
+ },
+
+ onEvent(event) {},
+};
+
+var attachmentBucketController = {
+ commands: {
+ cmd_selectAll: {
+ isEnabled() {
+ return true;
+ },
+ doCommand() {
+ gAttachmentBucket.selectAll();
+ },
+ },
+
+ cmd_delete: {
+ isEnabled() {
+ let cmdDelete = document.getElementById("cmd_delete");
+ let textValue = getComposeBundle().getString("removeAttachmentMsgs");
+ textValue = PluralForm.get(gAttachmentBucket.selectedCount, textValue);
+ let accesskeyValue = cmdDelete.getAttribute(
+ "valueRemoveAttachmentAccessKey"
+ );
+ cmdDelete.setAttribute("label", textValue);
+ cmdDelete.setAttribute("accesskey", accesskeyValue);
+
+ return gAttachmentBucket.selectedCount;
+ },
+ doCommand() {
+ RemoveSelectedAttachment();
+ },
+ },
+
+ cmd_openAttachment: {
+ isEnabled() {
+ return gAttachmentBucket.selectedCount == 1;
+ },
+ doCommand() {
+ OpenSelectedAttachment();
+ },
+ },
+
+ cmd_renameAttachment: {
+ isEnabled() {
+ return (
+ gAttachmentBucket.selectedCount == 1 &&
+ !gAttachmentBucket.selectedItem.uploading
+ );
+ },
+ doCommand() {
+ RenameSelectedAttachment();
+ },
+ },
+
+ cmd_moveAttachmentLeft: {
+ isEnabled() {
+ return (
+ gAttachmentBucket.selectedCount && !attachmentsSelectionIsBlock("top")
+ );
+ },
+ doCommand() {
+ moveSelectedAttachments("left");
+ },
+ },
+
+ cmd_moveAttachmentRight: {
+ isEnabled() {
+ return (
+ gAttachmentBucket.selectedCount &&
+ !attachmentsSelectionIsBlock("bottom")
+ );
+ },
+ doCommand() {
+ moveSelectedAttachments("right");
+ },
+ },
+
+ cmd_moveAttachmentBundleUp: {
+ isEnabled() {
+ return (
+ gAttachmentBucket.selectedCount > 1 && !attachmentsSelectionIsBlock()
+ );
+ },
+ doCommand() {
+ moveSelectedAttachments("bundleUp");
+ },
+ },
+
+ cmd_moveAttachmentBundleDown: {
+ isEnabled() {
+ return (
+ gAttachmentBucket.selectedCount > 1 && !attachmentsSelectionIsBlock()
+ );
+ },
+ doCommand() {
+ moveSelectedAttachments("bundleDown");
+ },
+ },
+
+ cmd_moveAttachmentTop: {
+ isEnabled() {
+ return (
+ gAttachmentBucket.selectedCount && !attachmentsSelectionIsBlock("top")
+ );
+ },
+ doCommand() {
+ moveSelectedAttachments("top");
+ },
+ },
+
+ cmd_moveAttachmentBottom: {
+ isEnabled() {
+ return (
+ gAttachmentBucket.selectedCount &&
+ !attachmentsSelectionIsBlock("bottom")
+ );
+ },
+ doCommand() {
+ moveSelectedAttachments("bottom");
+ },
+ },
+
+ cmd_sortAttachmentsToggle: {
+ isEnabled() {
+ let sortSelection;
+ let currSortOrder;
+ let isBlock;
+ let btnAscending;
+ let toggleCmd = document.getElementById("cmd_sortAttachmentsToggle");
+ let toggleBtn = document.getElementById("btn_sortAttachmentsToggle");
+ let sortDirection;
+ let btnLabelAttr;
+
+ if (
+ gAttachmentBucket.selectedCount > 1 &&
+ gAttachmentBucket.selectedCount < gAttachmentBucket.itemCount
+ ) {
+ // Sort selected attachments only, which needs at least 2 of them,
+ // but not all.
+ sortSelection = true;
+ currSortOrder = attachmentsSelectionGetSortOrder();
+ isBlock = attachmentsSelectionIsBlock();
+ // If current sorting is ascending AND it's a block; OR
+ // if current sorting is descending AND it's NOT a block yet:
+ // Offer toggle button face to sort descending.
+ // In all other cases, offer toggle button face to sort ascending.
+ btnAscending = !(
+ (currSortOrder == "ascending" && isBlock) ||
+ (currSortOrder == "descending" && !isBlock)
+ );
+ // Set sortDirection for toggleCmd, and respective button face.
+ if (btnAscending) {
+ sortDirection = "ascending";
+ btnLabelAttr = "label-selection-AZ";
+ } else {
+ sortDirection = "descending";
+ btnLabelAttr = "label-selection-ZA";
+ }
+ } else {
+ // gAttachmentBucket.selectedCount <= 1 or all attachments are selected.
+ // Sort all attachments.
+ sortSelection = false;
+ currSortOrder = attachmentsGetSortOrder();
+ btnAscending = !(currSortOrder == "ascending");
+ // Set sortDirection for toggleCmd, and respective button face.
+ if (btnAscending) {
+ sortDirection = "ascending";
+ btnLabelAttr = "label-AZ";
+ } else {
+ sortDirection = "descending";
+ btnLabelAttr = "label-ZA";
+ }
+ }
+
+ // Set the sort direction for toggleCmd.
+ toggleCmd.setAttribute("sortdirection", sortDirection);
+ // The button's icon is set dynamically via CSS involving the button's
+ // sortdirection attribute, which is forwarded by the command.
+ toggleBtn.setAttribute("label", toggleBtn.getAttribute(btnLabelAttr));
+
+ return sortSelection
+ ? !(currSortOrder == "equivalent" && isBlock)
+ : !(currSortOrder == "equivalent");
+ },
+ doCommand() {
+ moveSelectedAttachments("toggleSort");
+ },
+ },
+
+ cmd_convertCloud: {
+ isEnabled() {
+ // Hide the command entirely if Filelink is disabled, or if there are
+ // no cloud accounts.
+ let cmd = document.getElementById("cmd_convertCloud");
+
+ cmd.hidden =
+ !Services.prefs.getBoolPref("mail.cloud_files.enabled") ||
+ cloudFileAccounts.configuredAccounts.length == 0 ||
+ Services.io.offline;
+ if (cmd.hidden) {
+ return false;
+ }
+
+ for (let item of gAttachmentBucket.selectedItems) {
+ if (item.uploading) {
+ return false;
+ }
+ }
+ return true;
+ },
+ doCommand() {
+ // We should never actually call this, since the <command> node calls
+ // a different function.
+ },
+ },
+
+ cmd_convertAttachment: {
+ isEnabled() {
+ if (!Services.prefs.getBoolPref("mail.cloud_files.enabled")) {
+ return false;
+ }
+
+ for (let item of gAttachmentBucket.selectedItems) {
+ if (item.uploading) {
+ return false;
+ }
+ }
+ return true;
+ },
+ doCommand() {
+ convertSelectedToRegularAttachment();
+ },
+ },
+
+ cmd_cancelUpload: {
+ isEnabled() {
+ let cmd = document.getElementById(
+ "composeAttachmentContext_cancelUploadItem"
+ );
+
+ // If Filelink is disabled, hide this menuitem and bailout.
+ if (!Services.prefs.getBoolPref("mail.cloud_files.enabled")) {
+ cmd.hidden = true;
+ return false;
+ }
+
+ for (let item of gAttachmentBucket.selectedItems) {
+ if (item && item.uploading) {
+ cmd.hidden = false;
+ return true;
+ }
+ }
+
+ // Hide the command entirely if the selected attachments aren't cloud
+ // files.
+ // For some reason, the hidden property isn't propagating from the cmd
+ // to the menuitem.
+ cmd.hidden = true;
+ return false;
+ },
+ doCommand() {
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+
+ for (let item of gAttachmentBucket.selectedItems) {
+ if (item && item.uploading) {
+ let file = fileHandler.getFileFromURLSpec(item.attachment.url);
+ item.uploading.cancelFileUpload(window, file);
+ }
+ }
+ },
+ },
+ },
+
+ supportsCommand(aCommand) {
+ return aCommand in this.commands;
+ },
+
+ isCommandEnabled(aCommand) {
+ if (!this.supportsCommand(aCommand)) {
+ return false;
+ }
+ return this.commands[aCommand].isEnabled();
+ },
+
+ doCommand(aCommand) {
+ if (!this.supportsCommand(aCommand)) {
+ return;
+ }
+ var cmd = this.commands[aCommand];
+ if (!cmd.isEnabled()) {
+ return;
+ }
+ cmd.doCommand();
+ },
+
+ onEvent(event) {},
+};
+
+/**
+ * Start composing a new message.
+ */
+function goOpenNewMessage(aEvent) {
+ // If aEvent is passed, check if Shift key was pressed for composition in
+ // non-default format (HTML vs. plaintext).
+ let msgCompFormat =
+ aEvent && aEvent.shiftKey
+ ? Ci.nsIMsgCompFormat.OppositeOfDefault
+ : Ci.nsIMsgCompFormat.Default;
+
+ MailServices.compose.OpenComposeWindow(
+ null,
+ null,
+ null,
+ Ci.nsIMsgCompType.New,
+ msgCompFormat,
+ gCurrentIdentity,
+ null,
+ null
+ );
+}
+
+function QuoteSelectedMessage() {
+ var selectedURIs = GetSelectedMessages();
+ if (selectedURIs) {
+ gMsgCompose.allowRemoteContent = false;
+ for (let i = 0; i < selectedURIs.length; i++) {
+ gMsgCompose.quoteMessage(selectedURIs[i]);
+ }
+ }
+}
+
+function GetSelectedMessages() {
+ let mailWindow = Services.wm.getMostRecentWindow("mail:3pane");
+ if (!mailWindow) {
+ return null;
+ }
+ let tab = mailWindow.document.getElementById("tabmail").currentTabInfo;
+ if (tab.mode.name == "mail3PaneTab" && tab.message) {
+ return tab.chromeBrowser.contentWindow?.gDBView?.getURIsForSelection();
+ } else if (tab.mode.name == "mailMessageTab") {
+ return [tab.messageURI];
+ }
+ return null;
+}
+
+function SetupCommandUpdateHandlers() {
+ top.controllers.appendController(defaultController);
+ gAttachmentBucket.controllers.appendController(attachmentBucketController);
+
+ document
+ .getElementById("optionsMenuPopup")
+ .addEventListener("popupshowing", updateOptionItems, true);
+}
+
+function UnloadCommandUpdateHandlers() {
+ document
+ .getElementById("optionsMenuPopup")
+ .removeEventListener("popupshowing", updateOptionItems, true);
+
+ gAttachmentBucket.controllers.removeController(attachmentBucketController);
+ top.controllers.removeController(defaultController);
+}
+
+function CommandUpdate_MsgCompose() {
+ var focusedWindow = top.document.commandDispatcher.focusedWindow;
+
+ // we're just setting focus to where it was before
+ if (focusedWindow == gLastWindowToHaveFocus) {
+ return;
+ }
+
+ gLastWindowToHaveFocus = focusedWindow;
+ updateComposeItems();
+}
+
+function findbarFindReplace() {
+ focusMsgBody();
+ let findbar = document.getElementById("FindToolbar");
+ findbar.close();
+ goDoCommand("cmd_findReplace");
+ findbar.open();
+}
+
+function updateComposeItems() {
+ try {
+ // Edit Menu
+ goUpdateCommand("cmd_rewrap");
+
+ // Insert Menu
+ if (gMsgCompose && gMsgCompose.composeHTML) {
+ goUpdateCommand("cmd_renderedHTMLEnabler");
+ goUpdateCommand("cmd_fontColor");
+ goUpdateCommand("cmd_backgroundColor");
+ goUpdateCommand("cmd_decreaseFontStep");
+ goUpdateCommand("cmd_increaseFontStep");
+ goUpdateCommand("cmd_bold");
+ goUpdateCommand("cmd_italic");
+ goUpdateCommand("cmd_underline");
+ goUpdateCommand("cmd_removeStyles");
+ goUpdateCommand("cmd_ul");
+ goUpdateCommand("cmd_ol");
+ goUpdateCommand("cmd_indent");
+ goUpdateCommand("cmd_outdent");
+ goUpdateCommand("cmd_align");
+ goUpdateCommand("cmd_smiley");
+ }
+
+ // Options Menu
+ goUpdateCommand("cmd_spelling");
+
+ // Workaround to update 'Quote' toolbar button. (See bug 609926.)
+ goUpdateCommand("cmd_quoteMessage");
+ goUpdateCommand("cmd_toggleReturnReceipt");
+ } catch (e) {}
+}
+
+/**
+ * Disables or restores all toolbar items (menus/buttons) in the window.
+ *
+ * @param {boolean} disable - Meaning true = disable all items, false = restore
+ * items to the state stored before disabling them.
+ */
+function updateAllItems(disable) {
+ for (let item of document.querySelectorAll(
+ "menu, toolbarbutton, [command], [oncommand]"
+ )) {
+ if (disable) {
+ // Disable all items
+ item.setAttribute("stateBeforeSend", item.getAttribute("disabled"));
+ item.setAttribute("disabled", "disabled");
+ } else {
+ // Restore initial state
+ let stateBeforeSend = item.getAttribute("stateBeforeSend");
+ if (stateBeforeSend == "disabled" || stateBeforeSend == "true") {
+ item.setAttribute("disabled", stateBeforeSend);
+ } else {
+ item.removeAttribute("disabled");
+ }
+ item.removeAttribute("stateBeforeSend");
+ }
+ }
+}
+
+function InitFileSaveAsMenu() {
+ document
+ .getElementById("cmd_saveAsFile")
+ .setAttribute("checked", defaultSaveOperation == "file");
+ document
+ .getElementById("cmd_saveAsDraft")
+ .setAttribute("checked", defaultSaveOperation == "draft");
+ document
+ .getElementById("cmd_saveAsTemplate")
+ .setAttribute("checked", defaultSaveOperation == "template");
+}
+
+function isSmimeSigningConfigured() {
+ return !!gCurrentIdentity?.getUnicharAttribute("signing_cert_name");
+}
+
+function isSmimeEncryptionConfigured() {
+ return !!gCurrentIdentity?.getUnicharAttribute("encryption_cert_name");
+}
+
+function isPgpConfigured() {
+ return !!gCurrentIdentity?.getUnicharAttribute("openpgp_key_id");
+}
+
+function toggleGlobalSignMessage() {
+ gSendSigned = !gSendSigned;
+ gUserTouchedSendSigned = true;
+
+ updateAttachMyPubKey();
+ showSendEncryptedAndSigned();
+}
+
+function updateAttachMyPubKey() {
+ if (!gUserTouchedAttachMyPubKey) {
+ if (gSendSigned) {
+ gAttachMyPublicPGPKey = gCurrentIdentity.attachPgpKey;
+ } else {
+ gAttachMyPublicPGPKey = false;
+ }
+ }
+}
+
+function removeAutoDisableNotification() {
+ let notification = gComposeNotification.getNotificationWithValue(
+ "e2eeDisableNotification"
+ );
+ if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+}
+
+function toggleEncryptMessage() {
+ gSendEncrypted = !gSendEncrypted;
+
+ if (gSendEncrypted) {
+ removeAutoDisableNotification();
+ }
+
+ gUserTouchedSendEncrypted = true;
+ checkEncryptionState();
+}
+
+function toggleAttachMyPublicKey(target) {
+ gAttachMyPublicPGPKey = target.getAttribute("checked") != "true";
+ target.setAttribute("checked", gAttachMyPublicPGPKey);
+ gUserTouchedAttachMyPubKey = true;
+}
+
+function updateEncryptedSubject() {
+ let warnSubjectUnencrypted =
+ (!gSelectedTechnologyIsPGP && gSendEncrypted) ||
+ (isPgpConfigured() &&
+ gSelectedTechnologyIsPGP &&
+ gSendEncrypted &&
+ !gEncryptSubject);
+
+ document
+ .getElementById("msgSubject")
+ .classList.toggle("with-icon", warnSubjectUnencrypted);
+ document.getElementById("msgEncryptedSubjectIcon").hidden =
+ !warnSubjectUnencrypted;
+}
+
+function toggleEncryptedSubject() {
+ gEncryptSubject = !gEncryptSubject;
+ gUserTouchedEncryptSubject = true;
+ updateEncryptedSubject();
+}
+
+/**
+ * Update user interface elements
+ *
+ * @param {string} menu_id - suffix of the menu ID of the menu to update
+ */
+function setSecuritySettings(menu_id) {
+ let encItem = document.getElementById("menu_securityEncrypt" + menu_id);
+ encItem.setAttribute("checked", gSendEncrypted);
+
+ let disableSig = false;
+ let disableEnc = false;
+
+ if (gSelectedTechnologyIsPGP) {
+ if (!isPgpConfigured()) {
+ disableSig = true;
+ disableEnc = true;
+ }
+ } else {
+ if (!isSmimeSigningConfigured()) {
+ disableSig = true;
+ }
+ if (!isSmimeEncryptionConfigured()) {
+ disableEnc = true;
+ }
+ }
+
+ let sigItem = document.getElementById("menu_securitySign" + menu_id);
+ sigItem.setAttribute("checked", gSendSigned && !disableSig);
+
+ // The radio button to disable encryption is always active.
+ // This is necessary, even if the current identity doesn't have
+ // e2ee configured. If the user switches the sender identity of an
+ // email, we might keep encryption enabled, to not surprise the user.
+ // This means, we must always allow the user to disable encryption.
+ encItem.disabled = disableEnc && !gSendEncrypted;
+
+ sigItem.disabled = disableSig;
+
+ let pgpItem = document.getElementById("encTech_OpenPGP" + menu_id);
+ let smimeItem = document.getElementById("encTech_SMIME" + menu_id);
+
+ smimeItem.disabled =
+ !isSmimeSigningConfigured() && !isSmimeEncryptionConfigured();
+
+ let encryptSubjectItem = document.getElementById(
+ `menu_securityEncryptSubject${menu_id}`
+ );
+
+ pgpItem.setAttribute("checked", gSelectedTechnologyIsPGP);
+ smimeItem.setAttribute("checked", !gSelectedTechnologyIsPGP);
+ encryptSubjectItem.setAttribute(
+ "checked",
+ !disableEnc && gSelectedTechnologyIsPGP && gSendEncrypted && gEncryptSubject
+ );
+ encryptSubjectItem.setAttribute(
+ "disabled",
+ disableEnc || !gSelectedTechnologyIsPGP || !gSendEncrypted
+ );
+
+ document.getElementById("menu_recipientStatus" + menu_id).disabled =
+ disableEnc;
+ let manager = document.getElementById("menu_openManager" + menu_id);
+ manager.disabled = disableEnc;
+ manager.hidden = !gSelectedTechnologyIsPGP;
+}
+
+/**
+ * Show the message security status based on the selected encryption technology.
+ *
+ * @param {boolean} [isSending=false] - If the key assistant was triggered
+ * during a sending attempt.
+ */
+function showMessageComposeSecurityStatus(isSending = false) {
+ if (gSelectedTechnologyIsPGP) {
+ if (
+ Services.prefs.getBoolPref("mail.openpgp.key_assistant.enable", false)
+ ) {
+ gKeyAssistant.show(getEncryptionCompatibleRecipients(), isSending);
+ } else {
+ Recipients2CompFields(gMsgCompose.compFields);
+ window.openDialog(
+ "chrome://openpgp/content/ui/composeKeyStatus.xhtml",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ {
+ compFields: gMsgCompose.compFields,
+ currentIdentity: gCurrentIdentity,
+ }
+ );
+ checkEncryptionState();
+ }
+ } else {
+ Recipients2CompFields(gMsgCompose.compFields);
+ // Copy current flags to S/MIME composeSecure object.
+ gMsgCompose.compFields.composeSecure.requireEncryptMessage = gSendEncrypted;
+ gMsgCompose.compFields.composeSecure.signMessage = gSendSigned;
+ window.openDialog(
+ "chrome://messenger-smime/content/msgCompSecurityInfo.xhtml",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ {
+ compFields: gMsgCompose.compFields,
+ subject: document.getElementById("msgSubject").value,
+ isSigningCertAvailable:
+ gCurrentIdentity.getUnicharAttribute("signing_cert_name") != "",
+ isEncryptionCertAvailable:
+ gCurrentIdentity.getUnicharAttribute("encryption_cert_name") != "",
+ currentIdentity: gCurrentIdentity,
+ recipients: getEncryptionCompatibleRecipients(),
+ }
+ );
+ }
+}
+
+function msgComposeContextOnShowing(event) {
+ if (event.target.id != "msgComposeContext") {
+ return;
+ }
+
+ // gSpellChecker handles all spell checking related to the context menu,
+ // except whether or not spell checking is enabled. We need the editor's
+ // spell checker for that.
+ gSpellChecker.initFromRemote(
+ nsContextMenu.contentData.spellInfo,
+ nsContextMenu.contentData.actor.manager
+ );
+
+ let canSpell = gSpellChecker.canSpellCheck;
+ let showDictionaries = canSpell && gSpellChecker.enabled;
+ let onMisspelling = gSpellChecker.overMisspelling;
+ let showUndo = canSpell && gSpellChecker.canUndo();
+
+ document.getElementById("spellCheckSeparator").hidden = !canSpell;
+ document.getElementById("spellCheckEnable").hidden = !canSpell;
+ document
+ .getElementById("spellCheckEnable")
+ .setAttribute("checked", canSpell && gSpellCheckingEnabled);
+
+ document.getElementById("spellCheckAddToDictionary").hidden = !onMisspelling;
+ document.getElementById("spellCheckUndoAddToDictionary").hidden = !showUndo;
+ document.getElementById("spellCheckIgnoreWord").hidden = !onMisspelling;
+
+ // Suggestion list.
+ document.getElementById("spellCheckSuggestionsSeparator").hidden =
+ !onMisspelling && !showUndo;
+ let separator = document.getElementById("spellCheckAddSep");
+ separator.hidden = !onMisspelling;
+ if (onMisspelling) {
+ let addMenuItem = document.getElementById("spellCheckAddToDictionary");
+ let suggestionCount = gSpellChecker.addSuggestionsToMenu(
+ addMenuItem.parentNode,
+ separator,
+ nsContextMenu.contentData.spellInfo.spellSuggestions
+ );
+ document.getElementById("spellCheckNoSuggestions").hidden =
+ !suggestionCount == 0;
+ } else {
+ document.getElementById("spellCheckNoSuggestions").hidden = !false;
+ }
+
+ // Dictionary list.
+ document.getElementById("spellCheckDictionaries").hidden = !showDictionaries;
+ if (canSpell) {
+ let dictMenu = document.getElementById("spellCheckDictionariesMenu");
+ let dictSep = document.getElementById("spellCheckLanguageSeparator");
+ let count = gSpellChecker.addDictionaryListToMenu(dictMenu, dictSep);
+ dictSep.hidden = count == 0;
+ document.getElementById("spellCheckAddDictionariesMain").hidden = !false;
+ } else if (this.onSpellcheckable) {
+ // when there is no spellchecker but we might be able to spellcheck
+ // add the add to dictionaries item. This will ensure that people
+ // with no dictionaries will be able to download them
+ document.getElementById("spellCheckLanguageSeparator").hidden =
+ !showDictionaries;
+ document.getElementById("spellCheckAddDictionariesMain").hidden =
+ !showDictionaries;
+ } else {
+ document.getElementById("spellCheckAddDictionariesMain").hidden = !false;
+ }
+
+ updateEditItems();
+
+ // The rest of this block sends menu information to WebExtensions.
+
+ let editor = GetCurrentEditorElement();
+ let target = editor.contentDocument.elementFromPoint(
+ editor._contextX,
+ editor._contextY
+ );
+
+ let selectionInfo = SelectionUtils.getSelectionDetails(window);
+ let isContentSelected = !selectionInfo.docSelectionIsCollapsed;
+ let textSelected = selectionInfo.text;
+ let isTextSelected = !!textSelected.length;
+
+ // Set up early the right flags for editable / not editable.
+ let editFlags = SpellCheckHelper.isEditable(target, window);
+ let onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
+ let onEditable =
+ (editFlags &
+ (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) !==
+ 0;
+
+ let onImage = false;
+ let srcUrl = undefined;
+
+ if (target.nodeType == Node.ELEMENT_NODE) {
+ if (target instanceof Ci.nsIImageLoadingContent && target.currentURI) {
+ onImage = true;
+ srcUrl = target.currentURI.spec;
+ }
+ }
+
+ let onLink = false;
+ let linkText = undefined;
+ let linkUrl = undefined;
+
+ let link = target.closest("a");
+ if (link) {
+ onLink = true;
+ linkText =
+ link.textContent ||
+ link.getAttribute("title") ||
+ link.getAttribute("a") ||
+ link.href ||
+ "";
+ linkUrl = link.href;
+ }
+
+ let subject = {
+ menu: event.target,
+ tab: window,
+ isContentSelected,
+ isTextSelected,
+ onTextInput,
+ onLink,
+ onImage,
+ onEditable,
+ srcUrl,
+ linkText,
+ linkUrl,
+ selectionText: isTextSelected ? selectionInfo.fullText : undefined,
+ pageUrl: target.ownerGlobal.top.location.href,
+ onComposeBody: true,
+ };
+ subject.context = subject;
+ subject.wrappedJSObject = subject;
+
+ Services.obs.notifyObservers(subject, "on-prepare-contextmenu");
+ Services.obs.notifyObservers(subject, "on-build-contextmenu");
+}
+
+function msgComposeContextOnHiding(event) {
+ if (event.target.id != "msgComposeContext") {
+ return;
+ }
+
+ if (nsContextMenu.contentData.actor) {
+ nsContextMenu.contentData.actor.hiding();
+ }
+
+ nsContextMenu.contentData = null;
+ gSpellChecker.clearSuggestionsFromMenu();
+ gSpellChecker.clearDictionaryListFromMenu();
+ gSpellChecker.uninit();
+}
+
+function updateEditItems() {
+ goUpdateCommand("cmd_paste");
+ goUpdateCommand("cmd_pasteNoFormatting");
+ goUpdateCommand("cmd_pasteQuote");
+ goUpdateCommand("cmd_delete");
+ goUpdateCommand("cmd_renameAttachment");
+ goUpdateCommand("cmd_reorderAttachments");
+ goUpdateCommand("cmd_selectAll");
+ goUpdateCommand("cmd_openAttachment");
+ goUpdateCommand("cmd_findReplace");
+ goUpdateCommand("cmd_find");
+ goUpdateCommand("cmd_findNext");
+ goUpdateCommand("cmd_findPrev");
+}
+
+function updateViewItems() {
+ goUpdateCommand("cmd_toggleAttachmentPane");
+}
+
+function updateOptionItems() {
+ goUpdateCommand("cmd_quoteMessage");
+ goUpdateCommand("cmd_toggleReturnReceipt");
+}
+
+function updateAttachmentItems() {
+ goUpdateCommand("cmd_toggleAttachmentPane");
+ goUpdateCommand("cmd_attachCloud");
+ goUpdateCommand("cmd_convertCloud");
+ goUpdateCommand("cmd_convertAttachment");
+ goUpdateCommand("cmd_cancelUpload");
+ goUpdateCommand("cmd_delete");
+ goUpdateCommand("cmd_removeAllAttachments");
+ goUpdateCommand("cmd_renameAttachment");
+ updateReorderAttachmentsItems();
+ goUpdateCommand("cmd_selectAll");
+ goUpdateCommand("cmd_openAttachment");
+ goUpdateCommand("cmd_attachVCard");
+ goUpdateCommand("cmd_attachPublicKey");
+}
+
+function updateReorderAttachmentsItems() {
+ goUpdateCommand("cmd_reorderAttachments");
+ goUpdateCommand("cmd_moveAttachmentLeft");
+ goUpdateCommand("cmd_moveAttachmentRight");
+ goUpdateCommand("cmd_moveAttachmentBundleUp");
+ goUpdateCommand("cmd_moveAttachmentBundleDown");
+ goUpdateCommand("cmd_moveAttachmentTop");
+ goUpdateCommand("cmd_moveAttachmentBottom");
+ goUpdateCommand("cmd_sortAttachmentsToggle");
+}
+
+/**
+ * Update all the commands for sending a message to reflect their current state.
+ */
+function updateSendCommands(aHaveController) {
+ updateSendLock();
+ if (aHaveController) {
+ goUpdateCommand("cmd_sendButton");
+ goUpdateCommand("cmd_sendNow");
+ goUpdateCommand("cmd_sendLater");
+ goUpdateCommand("cmd_sendWithCheck");
+ } else {
+ goSetCommandEnabled(
+ "cmd_sendButton",
+ defaultController.isCommandEnabled("cmd_sendButton")
+ );
+ goSetCommandEnabled(
+ "cmd_sendNow",
+ defaultController.isCommandEnabled("cmd_sendNow")
+ );
+ goSetCommandEnabled(
+ "cmd_sendLater",
+ defaultController.isCommandEnabled("cmd_sendLater")
+ );
+ goSetCommandEnabled(
+ "cmd_sendWithCheck",
+ defaultController.isCommandEnabled("cmd_sendWithCheck")
+ );
+ }
+
+ let changed = false;
+ let currentStates = {};
+ let changedStates = {};
+ for (let state of ["cmd_sendNow", "cmd_sendLater"]) {
+ currentStates[state] = defaultController.isCommandEnabled(state);
+ if (
+ !gLastKnownComposeStates.hasOwnProperty(state) ||
+ gLastKnownComposeStates[state] != currentStates[state]
+ ) {
+ gLastKnownComposeStates[state] = currentStates[state];
+ changedStates[state] = currentStates[state];
+ changed = true;
+ }
+ }
+ if (changed) {
+ window.dispatchEvent(
+ new CustomEvent("compose-state-changed", { detail: changedStates })
+ );
+ }
+}
+
+function addAttachCloudMenuItems(aParentMenu) {
+ while (aParentMenu.hasChildNodes()) {
+ aParentMenu.lastChild.remove();
+ }
+
+ for (let account of cloudFileAccounts.configuredAccounts) {
+ if (
+ aParentMenu.lastElementChild &&
+ aParentMenu.lastElementChild.cloudFileUpload
+ ) {
+ aParentMenu.appendChild(document.createXULElement("menuseparator"));
+ }
+
+ let item = document.createXULElement("menuitem");
+ let iconURL = account.iconURL;
+ item.cloudFileAccount = account;
+ item.setAttribute(
+ "label",
+ cloudFileAccounts.getDisplayName(account) + "\u2026"
+ );
+ if (iconURL) {
+ item.setAttribute("class", `${item.localName}-iconic`);
+ item.setAttribute("image", iconURL);
+ }
+ aParentMenu.appendChild(item);
+
+ let previousUploads = account.getPreviousUploads();
+ let addedFiles = [];
+ for (let upload of previousUploads) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(upload.path);
+
+ // TODO: Figure out how to handle files that no longer exist on the filesystem.
+ if (!file.exists()) {
+ continue;
+ }
+ if (!addedFiles.find(f => f.name == upload.name || f.url == upload.url)) {
+ let fileItem = document.createXULElement("menuitem");
+ fileItem.cloudFileUpload = upload;
+ fileItem.cloudFileAccount = account;
+ fileItem.setAttribute("label", upload.name);
+ fileItem.setAttribute("class", "menuitem-iconic");
+ fileItem.setAttribute("image", "moz-icon://" + upload.name);
+ aParentMenu.appendChild(fileItem);
+ addedFiles.push({ name: upload.name, url: upload.url });
+ }
+ }
+ }
+}
+
+function addConvertCloudMenuItems(aParentMenu, aAfterNodeId, aRadioGroup) {
+ let afterNode = document.getElementById(aAfterNodeId);
+ while (afterNode.nextElementSibling) {
+ afterNode.nextElementSibling.remove();
+ }
+
+ if (!gAttachmentBucket.selectedItem.sendViaCloud) {
+ let item = document.getElementById(
+ "convertCloudMenuItems_popup_convertAttachment"
+ );
+ item.setAttribute("checked", "true");
+ }
+
+ for (let account of cloudFileAccounts.configuredAccounts) {
+ let item = document.createXULElement("menuitem");
+ let iconURL = account.iconURL;
+ item.cloudFileAccount = account;
+ item.setAttribute("label", cloudFileAccounts.getDisplayName(account));
+ item.setAttribute("type", "radio");
+ item.setAttribute("name", aRadioGroup);
+
+ if (
+ gAttachmentBucket.selectedItem.cloudFileAccount &&
+ gAttachmentBucket.selectedItem.cloudFileAccount.accountKey ==
+ account.accountKey
+ ) {
+ item.setAttribute("checked", "true");
+ } else if (iconURL) {
+ item.setAttribute("class", "menu-iconic");
+ item.setAttribute("image", iconURL);
+ }
+
+ aParentMenu.appendChild(item);
+ }
+
+ // Check if the cloudFile has an invalid account and deselect the default
+ // option, allowing to convert it back to a regular file.
+ if (
+ gAttachmentBucket.selectedItem.attachment.sendViaCloud &&
+ !gAttachmentBucket.selectedItem.cloudFileAccount
+ ) {
+ let regularItem = document.getElementById(
+ "convertCloudMenuItems_popup_convertAttachment"
+ );
+ regularItem.removeAttribute("checked");
+ }
+}
+
+async function updateAttachmentItemProperties(attachmentItem) {
+ // FIXME: The UI logic should be handled by the attachment list or item
+ // itself.
+ if (attachmentItem.uploading) {
+ // uploading/renaming
+ attachmentItem.setAttribute(
+ "tooltiptext",
+ getComposeBundle().getFormattedString("cloudFileUploadingTooltip", [
+ cloudFileAccounts.getDisplayName(attachmentItem.uploading),
+ ])
+ );
+ gAttachmentBucket.setCloudIcon(attachmentItem, "");
+ } else if (attachmentItem.attachment.sendViaCloud) {
+ let [tooltipUnknownAccountText, introText, titleText] =
+ await document.l10n.formatValues([
+ "cloud-file-unknown-account-tooltip",
+ {
+ id: "cloud-file-placeholder-intro",
+ args: { filename: attachmentItem.attachment.name },
+ },
+ {
+ id: "cloud-file-placeholder-title",
+ args: { filename: attachmentItem.attachment.name },
+ },
+ ]);
+
+ // uploaded
+ let tooltiptext;
+ if (attachmentItem.cloudFileAccount) {
+ tooltiptext = getComposeBundle().getFormattedString(
+ "cloudFileUploadedTooltip",
+ [cloudFileAccounts.getDisplayName(attachmentItem.cloudFileAccount)]
+ );
+ } else {
+ tooltiptext = tooltipUnknownAccountText;
+ }
+ attachmentItem.setAttribute("tooltiptext", tooltiptext);
+
+ gAttachmentBucket.setAttachmentName(
+ attachmentItem,
+ attachmentItem.attachment.name
+ );
+ gAttachmentBucket.setCloudIcon(
+ attachmentItem,
+ attachmentItem.cloudFileUpload.serviceIcon
+ );
+
+ // Update the CloudPartHeaderData, if there is a valid cloudFileUpload.
+ if (attachmentItem.cloudFileUpload) {
+ let json = JSON.stringify(attachmentItem.cloudFileUpload);
+ // Convert 16bit JavaScript string to a byteString, to make it work with
+ // btoa().
+ attachmentItem.attachment.cloudPartHeaderData = btoa(
+ MailStringUtils.stringToByteString(json)
+ );
+ }
+
+ // Update the cloudFile placeholder file.
+ attachmentItem.attachment.htmlAnnotation = `<!DOCTYPE html>
+<html>
+ <head>
+ <title>${titleText}</title>
+ <meta charset="utf-8" />
+ </head>
+ <body>
+ <div style="padding: 15px; font-family: Calibri, sans-serif;">
+ <div style="margin-bottom: 15px;" id="cloudAttachmentListHeader">${introText}</div>
+ <ul>${
+ (
+ await gCloudAttachmentLinkManager._createNode(
+ document,
+ attachmentItem.cloudFileUpload,
+ true
+ )
+ ).outerHTML
+ }</ul>
+ </div>
+ </body>
+</html>`;
+
+ // Calculate size of placeholder attachment.
+ attachmentItem.cloudHtmlFileSize = new TextEncoder().encode(
+ attachmentItem.attachment.htmlAnnotation
+ ).length;
+ } else {
+ // local
+ attachmentItem.setAttribute("tooltiptext", attachmentItem.attachment.url);
+ gAttachmentBucket.setAttachmentName(
+ attachmentItem,
+ attachmentItem.attachment.name
+ );
+ gAttachmentBucket.setCloudIcon(attachmentItem, "");
+
+ // Remove placeholder file size information.
+ delete attachmentItem.cloudHtmlFileSize;
+ }
+ updateAttachmentPane();
+}
+
+async function showLocalizedCloudFileAlert(
+ ex,
+ provider = ex.cloudProvider,
+ filename = ex.cloudFileName
+) {
+ let bundle = getComposeBundle();
+ let localizedTitle, localizedMessage;
+
+ switch (ex.result) {
+ case cloudFileAccounts.constants.uploadCancelled:
+ // No alerts for cancelled uploads.
+ return;
+ case cloudFileAccounts.constants.deleteErr:
+ localizedTitle = bundle.getString("errorCloudFileDeletion.title");
+ localizedMessage = bundle.getFormattedString(
+ "errorCloudFileDeletion.message",
+ [provider, filename]
+ );
+ break;
+ case cloudFileAccounts.constants.offlineErr:
+ localizedTitle = await l10nCompose.formatValue(
+ "cloud-file-connection-error-title"
+ );
+ localizedMessage = await l10nCompose.formatValue(
+ "cloud-file-connection-error",
+ {
+ provider,
+ }
+ );
+ break;
+ case cloudFileAccounts.constants.authErr:
+ localizedTitle = bundle.getString("errorCloudFileAuth.title");
+ localizedMessage = bundle.getFormattedString(
+ "errorCloudFileAuth.message",
+ [provider]
+ );
+ break;
+ case cloudFileAccounts.constants.uploadErrWithCustomMessage:
+ localizedTitle = await l10nCompose.formatValue(
+ "cloud-file-upload-error-with-custom-message-title",
+ {
+ provider,
+ filename,
+ }
+ );
+ localizedMessage = ex.message;
+ break;
+ case cloudFileAccounts.constants.uploadErr:
+ localizedTitle = bundle.getString("errorCloudFileUpload.title");
+ localizedMessage = bundle.getFormattedString(
+ "errorCloudFileUpload.message",
+ [provider, filename]
+ );
+ break;
+ case cloudFileAccounts.constants.uploadWouldExceedQuota:
+ localizedTitle = bundle.getString("errorCloudFileQuota.title");
+ localizedMessage = bundle.getFormattedString(
+ "errorCloudFileQuota.message",
+ [provider, filename]
+ );
+ break;
+ case cloudFileAccounts.constants.uploadExceedsFileLimit:
+ localizedTitle = bundle.getString("errorCloudFileLimit.title");
+ localizedMessage = bundle.getFormattedString(
+ "errorCloudFileLimit.message",
+ [provider, filename]
+ );
+ break;
+ case cloudFileAccounts.constants.renameNotSupported:
+ localizedTitle = await l10nCompose.formatValue(
+ "cloud-file-rename-error-title"
+ );
+ localizedMessage = await l10nCompose.formatValue(
+ "cloud-file-rename-not-supported",
+ {
+ provider,
+ }
+ );
+ break;
+ case cloudFileAccounts.constants.renameErrWithCustomMessage:
+ localizedTitle = await l10nCompose.formatValue(
+ "cloud-file-rename-error-with-custom-message-title",
+ {
+ provider,
+ filename,
+ }
+ );
+ localizedMessage = ex.message;
+ break;
+ case cloudFileAccounts.constants.renameErr:
+ localizedTitle = await l10nCompose.formatValue(
+ "cloud-file-rename-error-title"
+ );
+ localizedMessage = await l10nCompose.formatValue(
+ "cloud-file-rename-error",
+ {
+ provider,
+ filename,
+ }
+ );
+ break;
+ case cloudFileAccounts.constants.attachmentErr:
+ localizedTitle = await l10nCompose.formatValue(
+ "cloud-file-attachment-error-title"
+ );
+ localizedMessage = await l10nCompose.formatValue(
+ "cloud-file-attachment-error",
+ {
+ filename,
+ }
+ );
+ break;
+ case cloudFileAccounts.constants.accountErr:
+ localizedTitle = await l10nCompose.formatValue(
+ "cloud-file-account-error-title"
+ );
+ localizedMessage = await l10nCompose.formatValue(
+ "cloud-file-account-error",
+ {
+ filename,
+ }
+ );
+ break;
+ default:
+ localizedTitle = bundle.getString("errorCloudFileOther.title");
+ localizedMessage = bundle.getFormattedString(
+ "errorCloudFileOther.message",
+ [provider]
+ );
+ }
+
+ Services.prompt.alert(window, localizedTitle, localizedMessage);
+}
+
+/**
+ * @typedef UpdateSettings
+ * @property {CloudFileAccount} [cloudFileAccount] - cloud file account to store
+ * the attachment
+ * @property {CloudFileUpload} [relatedCloudFileUpload] - information about an
+ * already uploaded file this upload is related to, e.g. renaming a repeatedly
+ * used cloud file or updating the content of a cloud file
+ * @property {nsIFile} [file] - file to replace the current attachments content
+ * @property {string} [name] - name to replace the current attachments name
+ */
+
+/**
+ * Update the name and or the content of an attachment, as well as its local/cloud
+ * state.
+ *
+ * @param {DOMNode} attachmentItem - the existing attachmentItem
+ * @param {UpdateSettings} [updateSettings] - object defining how to update the
+ * attachment
+ */
+async function UpdateAttachment(attachmentItem, updateSettings = {}) {
+ if (!attachmentItem || !attachmentItem.attachment) {
+ throw new Error("Unexpected: Invalid attachment item.");
+ }
+
+ let originalAttachment = Object.assign({}, attachmentItem.attachment);
+ let eventOnDone = false;
+
+ // Ignore empty or falsy names.
+ let name = updateSettings.name || attachmentItem.attachment.name;
+
+ let destCloudFileAccount = updateSettings.hasOwnProperty("cloudFileAccount")
+ ? updateSettings.cloudFileAccount
+ : attachmentItem.cloudFileAccount;
+
+ try {
+ if (
+ // Bypass upload and set provided relatedCloudFileUpload.
+ updateSettings.relatedCloudFileUpload &&
+ updateSettings.cloudFileAccount &&
+ updateSettings.cloudFileAccount.reuseUploads &&
+ !updateSettings.file &&
+ !updateSettings.name
+ ) {
+ attachmentItem.attachment.sendViaCloud = true;
+ attachmentItem.attachment.contentLocation =
+ updateSettings.relatedCloudFileUpload.url;
+ attachmentItem.attachment.cloudFileAccountKey =
+ updateSettings.cloudFileAccount.accountKey;
+
+ attachmentItem.cloudFileAccount = updateSettings.cloudFileAccount;
+ attachmentItem.cloudFileUpload = updateSettings.relatedCloudFileUpload;
+ gAttachmentBucket.setCloudIcon(
+ attachmentItem,
+ updateSettings.relatedCloudFileUpload.serviceIcon
+ );
+
+ eventOnDone = new CustomEvent("attachment-uploaded", {
+ bubbles: true,
+ cancelable: true,
+ });
+ } else if (
+ // Handle a local -> local replace/rename.
+ !attachmentItem.attachment.sendViaCloud &&
+ !updateSettings.hasOwnProperty("cloudFileAccount")
+ ) {
+ // Both modes - rename and replace - require the same UI handling.
+ eventOnDone = new CustomEvent("attachment-renamed", {
+ bubbles: true,
+ cancelable: true,
+ detail: originalAttachment,
+ });
+ } else if (
+ // Handle a cloud -> local conversion.
+ attachmentItem.attachment.sendViaCloud &&
+ updateSettings.cloudFileAccount === null
+ ) {
+ // Throw if the linked local file does not exists (i.e. invalid draft).
+ if (!(await IOUtils.exists(attachmentItem.cloudFileUpload.path))) {
+ throw Components.Exception(
+ `CloudFile Error: Attachment file not found: ${attachmentItem.cloudFileUpload.path}`,
+ cloudFileAccounts.constants.attachmentErr
+ );
+ }
+
+ if (attachmentItem.cloudFileAccount) {
+ // A cloud delete error is not considered to be a fatal error. It is
+ // not preventing the attachment from being removed from the composer.
+ attachmentItem.cloudFileAccount
+ .deleteFile(window, attachmentItem.cloudFileUpload.id)
+ .catch(ex => console.warn(ex.message));
+ }
+ // Clean up attachment from cloud bits.
+ attachmentItem.attachment.sendViaCloud = false;
+ attachmentItem.attachment.htmlAnnotation = "";
+ attachmentItem.attachment.contentLocation = "";
+ attachmentItem.attachment.cloudFileAccountKey = "";
+ attachmentItem.attachment.cloudPartHeaderData = "";
+ delete attachmentItem.cloudFileAccount;
+ delete attachmentItem.cloudFileUpload;
+
+ eventOnDone = new CustomEvent("attachment-converted-to-regular", {
+ bubbles: true,
+ cancelable: true,
+ detail: originalAttachment,
+ });
+ } else if (
+ // Exit early if offline.
+ Services.io.offline
+ ) {
+ throw Components.Exception(
+ "Connection error: Offline",
+ cloudFileAccounts.constants.offlineErr
+ );
+ } else {
+ // Handle a cloud -> cloud move/rename or a local -> cloud upload.
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+
+ let mode = "upload";
+ if (attachmentItem.attachment.sendViaCloud) {
+ // Throw if the used cloudFile account does not exists (invalid draft,
+ // disabled add-on, removed account).
+ if (
+ !destCloudFileAccount ||
+ !cloudFileAccounts.getAccount(destCloudFileAccount.accountKey)
+ ) {
+ throw Components.Exception(
+ `CloudFile Error: Account not found: ${destCloudFileAccount?.accountKey}`,
+ cloudFileAccounts.constants.accountErr
+ );
+ }
+
+ if (
+ attachmentItem.cloudFileUpload &&
+ attachmentItem.cloudFileAccount == destCloudFileAccount &&
+ !updateSettings.file &&
+ !destCloudFileAccount.isReusedUpload(attachmentItem.cloudFileUpload)
+ ) {
+ mode = "rename";
+ } else {
+ mode = "move";
+ // Throw if the linked local file does not exists (invalid draft, removed
+ // local file).
+ if (
+ !fileHandler
+ .getFileFromURLSpec(attachmentItem.attachment.url)
+ .exists()
+ ) {
+ throw Components.Exception(
+ `CloudFile Error: Attachment file not found: ${
+ fileHandler.getFileFromURLSpec(attachmentItem.attachment.url)
+ .path
+ }`,
+ cloudFileAccounts.constants.attachmentErr
+ );
+ }
+ if (!(await IOUtils.exists(attachmentItem.cloudFileUpload.path))) {
+ throw Components.Exception(
+ `CloudFile Error: Attachment file not found: ${attachmentItem.cloudFileUpload.path}`,
+ cloudFileAccounts.constants.attachmentErr
+ );
+ }
+ }
+ }
+
+ // Notify the UI that we're starting the upload process: disable send commands
+ // and show a "connecting" icon for the attachment.
+ gNumUploadingAttachments++;
+ updateSendCommands(true);
+
+ attachmentItem.uploading = destCloudFileAccount;
+ await updateAttachmentItemProperties(attachmentItem);
+
+ const eventsOnStart = {
+ upload: "attachment-uploading",
+ move: "attachment-moving",
+ };
+ if (eventsOnStart[mode]) {
+ attachmentItem.dispatchEvent(
+ new CustomEvent(eventsOnStart[mode], {
+ bubbles: true,
+ cancelable: true,
+ detail: attachmentItem.attachment,
+ })
+ );
+ }
+
+ try {
+ let upload;
+ if (mode == "rename") {
+ upload = await destCloudFileAccount.renameFile(
+ window,
+ attachmentItem.cloudFileUpload.id,
+ name
+ );
+ } else {
+ let file =
+ updateSettings.file ||
+ fileHandler.getFileFromURLSpec(attachmentItem.attachment.url);
+
+ upload = await destCloudFileAccount.uploadFile(
+ window,
+ file,
+ name,
+ updateSettings.relatedCloudFileUpload
+ );
+
+ attachmentItem.cloudFileAccount = destCloudFileAccount;
+ attachmentItem.attachment.sendViaCloud = true;
+ attachmentItem.attachment.cloudFileAccountKey =
+ destCloudFileAccount.accountKey;
+
+ Services.telemetry.keyedScalarAdd(
+ "tb.filelink.uploaded_size",
+ destCloudFileAccount.type,
+ file.fileSize
+ );
+ }
+
+ attachmentItem.cloudFileUpload = upload;
+ attachmentItem.attachment.contentLocation = upload.url;
+
+ const eventsOnSuccess = {
+ upload: "attachment-uploaded",
+ move: "attachment-moved",
+ rename: "attachment-renamed",
+ };
+ if (eventsOnSuccess[mode]) {
+ eventOnDone = new CustomEvent(eventsOnSuccess[mode], {
+ bubbles: true,
+ cancelable: true,
+ detail: originalAttachment,
+ });
+ }
+ } catch (ex) {
+ const eventsOnFailure = {
+ upload: "attachment-upload-failed",
+ move: "attachment-move-failed",
+ };
+ if (eventsOnFailure[mode]) {
+ eventOnDone = new CustomEvent(eventsOnFailure[mode], {
+ bubbles: true,
+ cancelable: true,
+ detail: ex.result,
+ });
+ }
+ throw ex;
+ } finally {
+ attachmentItem.uploading = false;
+ gNumUploadingAttachments--;
+ updateSendCommands(true);
+ }
+ }
+
+ // Update the local attachment.
+ if (updateSettings.file) {
+ let attachment = FileToAttachment(updateSettings.file);
+ attachmentItem.attachment.size = attachment.size;
+ attachmentItem.attachment.url = attachment.url;
+ }
+ attachmentItem.attachment.name = name;
+
+ AttachmentsChanged();
+ // Update cmd_sortAttachmentsToggle because replacing/renaming may change the
+ // current sort order.
+ goUpdateCommand("cmd_sortAttachmentsToggle");
+ } catch (ex) {
+ // Attach provider and fileName to the Exception, so showLocalizedCloudFileAlert()
+ // can display the proper alert message.
+ ex.cloudProvider = destCloudFileAccount
+ ? cloudFileAccounts.getDisplayName(destCloudFileAccount)
+ : "";
+ ex.cloudFileName = originalAttachment?.name || name;
+ throw ex;
+ } finally {
+ await updateAttachmentItemProperties(attachmentItem);
+ if (eventOnDone) {
+ attachmentItem.dispatchEvent(eventOnDone);
+ }
+ }
+}
+
+function attachToCloud(event) {
+ gMsgCompose.allowRemoteContent = true;
+ if (event.target.cloudFileUpload) {
+ attachToCloudRepeat(
+ event.target.cloudFileUpload,
+ event.target.cloudFileAccount
+ );
+ } else {
+ attachToCloudNew(event.target.cloudFileAccount);
+ }
+ event.stopPropagation();
+}
+
+/**
+ * Attach a file that has already been uploaded to a cloud provider.
+ *
+ * @param {object} upload - the cloudFileUpload of the already uploaded file
+ * @param {object} account - the cloudFileAccount of the already uploaded file
+ */
+async function attachToCloudRepeat(upload, account) {
+ gMsgCompose.allowRemoteContent = true;
+ let file = FileUtils.File(upload.path);
+ let attachment = FileToAttachment(file);
+ attachment.name = upload.name;
+
+ let addedAttachmentItems = await AddAttachments([attachment]);
+ if (addedAttachmentItems.length > 0) {
+ try {
+ await UpdateAttachment(addedAttachmentItems[0], {
+ cloudFileAccount: account,
+ relatedCloudFileUpload: upload,
+ });
+ } catch (ex) {
+ showLocalizedCloudFileAlert(ex);
+ }
+ }
+}
+
+/**
+ * Prompt the user for a list of files to attach via a cloud provider.
+ *
+ * @param aAccount the cloud provider to upload the files to
+ */
+async function attachToCloudNew(aAccount) {
+ // We need to let the user pick local file(s) to upload to the cloud and
+ // gather url(s) to those files.
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(
+ window,
+ getComposeBundle().getFormattedString("chooseFileToAttachViaCloud", [
+ cloudFileAccounts.getDisplayName(aAccount),
+ ]),
+ Ci.nsIFilePicker.modeOpenMultiple
+ );
+
+ var lastDirectory = GetLastAttachDirectory();
+ if (lastDirectory) {
+ fp.displayDirectory = lastDirectory;
+ }
+
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ let rv = await new Promise(resolve => fp.open(resolve));
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.files) {
+ return;
+ }
+
+ let files = [...fp.files];
+ let attachments = files.map(f => FileToAttachment(f));
+ let addedAttachmentItems = await AddAttachments(attachments);
+ SetLastAttachDirectory(files[files.length - 1]);
+
+ let promises = [];
+ for (let attachmentItem of addedAttachmentItems) {
+ promises.push(
+ UpdateAttachment(attachmentItem, { cloudFileAccount: aAccount }).catch(
+ ex => {
+ RemoveAttachments([attachmentItem]);
+ showLocalizedCloudFileAlert(ex);
+ }
+ )
+ );
+ }
+
+ await Promise.all(promises);
+}
+
+/**
+ * Convert an array of attachments to cloud attachments.
+ *
+ * @param aItems an array of <attachmentitem>s containing the attachments in
+ * question
+ * @param aAccount the cloud account to upload the files to
+ */
+async function convertListItemsToCloudAttachment(aItems, aAccount) {
+ gMsgCompose.allowRemoteContent = true;
+ let promises = [];
+ for (let item of aItems) {
+ // Bail out, if we would convert to the current account.
+ if (
+ item.attachment.sendViaCloud &&
+ item.cloudFileAccount &&
+ item.cloudFileAccount == aAccount
+ ) {
+ continue;
+ }
+ promises.push(
+ UpdateAttachment(item, { cloudFileAccount: aAccount }).catch(
+ showLocalizedCloudFileAlert
+ )
+ );
+ }
+ await Promise.all(promises);
+}
+
+/**
+ * Convert the selected attachments to cloud attachments.
+ *
+ * @param aAccount the cloud account to upload the files to
+ */
+function convertSelectedToCloudAttachment(aAccount) {
+ convertListItemsToCloudAttachment(
+ [...gAttachmentBucket.selectedItems],
+ aAccount
+ );
+}
+
+/**
+ * Convert an array of nsIMsgAttachments to cloud attachments.
+ *
+ * @param aAttachments an array of nsIMsgAttachments
+ * @param aAccount the cloud account to upload the files to
+ */
+function convertToCloudAttachment(aAttachments, aAccount) {
+ let items = [];
+ for (let attachment of aAttachments) {
+ let item = gAttachmentBucket.findItemForAttachment(attachment);
+ if (item) {
+ items.push(item);
+ }
+ }
+
+ convertListItemsToCloudAttachment(items, aAccount);
+}
+
+/**
+ * Convert an array of attachments to regular (non-cloud) attachments.
+ *
+ * @param aItems an array of <attachmentitem>s containing the attachments in
+ * question
+ */
+async function convertListItemsToRegularAttachment(aItems) {
+ let promises = [];
+ for (let item of aItems) {
+ if (!item.attachment.sendViaCloud) {
+ continue;
+ }
+ promises.push(
+ UpdateAttachment(item, { cloudFileAccount: null }).catch(
+ showLocalizedCloudFileAlert
+ )
+ );
+ }
+ await Promise.all(promises);
+}
+
+/**
+ * Convert the selected attachments to regular (non-cloud) attachments.
+ */
+function convertSelectedToRegularAttachment() {
+ return convertListItemsToRegularAttachment([
+ ...gAttachmentBucket.selectedItems,
+ ]);
+}
+
+/**
+ * Convert an array of nsIMsgAttachments to regular (non-cloud) attachments.
+ *
+ * @param aAttachments an array of nsIMsgAttachments
+ */
+function convertToRegularAttachment(aAttachments) {
+ let items = [];
+ for (let attachment of aAttachments) {
+ let item = gAttachmentBucket.findItemForAttachment(attachment);
+ if (item) {
+ items.push(item);
+ }
+ }
+
+ return convertListItemsToRegularAttachment(items);
+}
+
+/* messageComposeOfflineQuitObserver is notified whenever the network
+ * connection status has switched to offline, or when the application
+ * has received a request to quit.
+ */
+var messageComposeOfflineQuitObserver = {
+ observe(aSubject, aTopic, aData) {
+ // sanity checks
+ if (aTopic == "network:offline-status-changed") {
+ MessageComposeOfflineStateChanged(Services.io.offline);
+ } else if (
+ aTopic == "quit-application-requested" &&
+ aSubject instanceof Ci.nsISupportsPRBool &&
+ !aSubject.data
+ ) {
+ // Check whether to veto the quit request
+ // (unless another observer already did).
+ aSubject.data = !ComposeCanClose();
+ }
+ },
+};
+
+function AddMessageComposeOfflineQuitObserver() {
+ Services.obs.addObserver(
+ messageComposeOfflineQuitObserver,
+ "network:offline-status-changed"
+ );
+ Services.obs.addObserver(
+ messageComposeOfflineQuitObserver,
+ "quit-application-requested"
+ );
+
+ // set the initial state of the send button
+ MessageComposeOfflineStateChanged(Services.io.offline);
+}
+
+function RemoveMessageComposeOfflineQuitObserver() {
+ Services.obs.removeObserver(
+ messageComposeOfflineQuitObserver,
+ "network:offline-status-changed"
+ );
+ Services.obs.removeObserver(
+ messageComposeOfflineQuitObserver,
+ "quit-application-requested"
+ );
+}
+
+function MessageComposeOfflineStateChanged(goingOffline) {
+ try {
+ var sendButton = document.getElementById("button-send");
+ var sendNowMenuItem = document.getElementById("menu-item-send-now");
+
+ if (!gSavedSendNowKey) {
+ gSavedSendNowKey = sendNowMenuItem.getAttribute("key");
+ }
+
+ // don't use goUpdateCommand here ... the defaultController might not be installed yet
+ updateSendCommands(false);
+
+ if (goingOffline) {
+ sendButton.label = sendButton.getAttribute("later_label");
+ sendButton.setAttribute(
+ "tooltiptext",
+ sendButton.getAttribute("later_tooltiptext")
+ );
+ sendNowMenuItem.removeAttribute("key");
+ } else {
+ sendButton.label = sendButton.getAttribute("now_label");
+ sendButton.setAttribute(
+ "tooltiptext",
+ sendButton.getAttribute("now_tooltiptext")
+ );
+ if (gSavedSendNowKey) {
+ sendNowMenuItem.setAttribute("key", gSavedSendNowKey);
+ }
+ }
+ } catch (e) {}
+}
+
+function DoCommandPrint() {
+ let browser = GetCurrentEditorElement();
+ browser.contentDocument.title =
+ document.getElementById("msgSubject").value.trim() ||
+ getComposeBundle().getString("defaultSubject");
+ PrintUtils.startPrintWindow(browser.browsingContext, {});
+}
+
+/**
+ * Locks/Unlocks the window widgets while a message is being saved/sent.
+ * Locking means to disable all possible items in the window so that
+ * the user can't click/activate anything.
+ *
+ * @param aDisable true = lock the window. false = unlock the window.
+ */
+function ToggleWindowLock(aDisable) {
+ if (aDisable) {
+ // Save the active element so we can focus it again.
+ ToggleWindowLock.activeElement = document.activeElement;
+ }
+ gWindowLocked = aDisable;
+ updateAllItems(aDisable);
+ updateEditableFields(aDisable);
+ if (!aDisable) {
+ updateComposeItems();
+ // Refocus what had focus when the lock began.
+ ToggleWindowLock.activeElement?.focus();
+ }
+}
+
+/* This function will go away soon as now arguments are passed to the window using a object of type nsMsgComposeParams instead of a string */
+function GetArgs(originalData) {
+ var args = {};
+
+ if (originalData == "") {
+ return null;
+ }
+
+ var data = "";
+ var separator = String.fromCharCode(1);
+
+ var quoteChar = "";
+ var prevChar = "";
+ var nextChar = "";
+ for (let i = 0; i < originalData.length; i++, prevChar = aChar) {
+ var aChar = originalData.charAt(i);
+ var aCharCode = originalData.charCodeAt(i);
+ if (i < originalData.length - 1) {
+ nextChar = originalData.charAt(i + 1);
+ } else {
+ nextChar = "";
+ }
+
+ if (aChar == quoteChar && (nextChar == "," || nextChar == "")) {
+ quoteChar = "";
+ data += aChar;
+ } else if ((aCharCode == 39 || aCharCode == 34) && prevChar == "=") {
+ // quote or double quote
+ if (quoteChar == "") {
+ quoteChar = aChar;
+ }
+ data += aChar;
+ } else if (aChar == ",") {
+ if (quoteChar == "") {
+ data += separator;
+ } else {
+ data += aChar;
+ }
+ } else {
+ data += aChar;
+ }
+ }
+
+ var pairs = data.split(separator);
+ // dump("Compose: argument: {" + data + "}\n");
+
+ for (let i = pairs.length - 1; i >= 0; i--) {
+ var pos = pairs[i].indexOf("=");
+ if (pos == -1) {
+ continue;
+ }
+ var argname = pairs[i].substring(0, pos);
+ var argvalue = pairs[i].substring(pos + 1);
+ if (argvalue.startsWith("'") && argvalue.endsWith("'")) {
+ args[argname] = argvalue.substring(1, argvalue.length - 1);
+ } else {
+ try {
+ args[argname] = decodeURIComponent(argvalue);
+ } catch (e) {
+ args[argname] = argvalue;
+ }
+ }
+ // dump("[" + argname + "=" + args[argname] + "]\n");
+ }
+ return args;
+}
+
+function ComposeFieldsReady() {
+ // If we are in plain text, we need to set the wrap column
+ if (!gMsgCompose.composeHTML) {
+ try {
+ gMsgCompose.editor.wrapWidth = gMsgCompose.wrapLength;
+ } catch (e) {
+ dump("### textEditor.wrapWidth exception text: " + e + " - failed\n");
+ }
+ }
+
+ CompFields2Recipients(gMsgCompose.compFields);
+ SetComposeWindowTitle();
+ updateEditableFields(false);
+ gLoadingComplete = true;
+
+ // Set up observers to recheck limit and encyption on recipients change.
+ observeRecipientsChange();
+
+ // Perform the initial checks.
+ checkPublicRecipientsLimit();
+ checkEncryptionState();
+}
+
+/**
+ * Set up observers to recheck limit and encyption on recipients change.
+ */
+function observeRecipientsChange() {
+ // Observe childList changes of `To` and `Cc` address rows to check if we need
+ // to show the public bulk recipients notification according to the threshold.
+ // So far we're only counting recipient pills, not plain text addresses.
+ gRecipientObserver = new MutationObserver(function (mutations) {
+ if (mutations.some(m => m.type == "childList")) {
+ checkPublicRecipientsLimit();
+ }
+ });
+ gRecipientObserver.observe(document.getElementById("toAddrContainer"), {
+ childList: true,
+ });
+ gRecipientObserver.observe(document.getElementById("ccAddrContainer"), {
+ childList: true,
+ });
+
+ function callCheckEncryptionState() {
+ // We must not pass the parameters that we get from observing.
+ checkEncryptionState();
+ }
+
+ gRecipientKeysObserver = new MutationObserver(callCheckEncryptionState);
+ gRecipientKeysObserver.observe(document.getElementById("toAddrContainer"), {
+ childList: true,
+ });
+ gRecipientKeysObserver.observe(document.getElementById("ccAddrContainer"), {
+ childList: true,
+ });
+ gRecipientKeysObserver.observe(document.getElementById("bccAddrContainer"), {
+ childList: true,
+ });
+}
+
+// checks if the passed in string is a mailto url, if it is, generates nsIMsgComposeParams
+// for the url and returns them.
+function handleMailtoArgs(mailtoUrl) {
+ // see if the string is a mailto url....do this by checking the first 7 characters of the string
+ if (mailtoUrl.toLowerCase().startsWith("mailto:")) {
+ // if it is a mailto url, turn the mailto url into a MsgComposeParams object....
+ let uri = Services.io.newURI(mailtoUrl);
+
+ if (uri) {
+ return MailServices.compose.getParamsForMailto(uri);
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Handle ESC keypress from composition window for
+ * notifications with close button in the
+ * attachmentNotificationBox.
+ */
+function handleEsc() {
+ let activeElement = document.activeElement;
+
+ if (activeElement.id == "messageEditor") {
+ // Focus within the message body.
+ let findbar = document.getElementById("FindToolbar");
+ if (!findbar.hidden) {
+ // If findbar is visible hide it.
+ // Focus on the findbar is handled by findbar itself.
+ findbar.close();
+ } else {
+ // Close the most recently shown notification.
+ gComposeNotification.currentNotification?.close();
+ }
+ return;
+ }
+
+ // If focus is within a notification, close the corresponding notification.
+ for (let notification of gComposeNotification.allNotifications) {
+ if (notification.contains(activeElement)) {
+ notification.close();
+ return;
+ }
+ }
+}
+
+/**
+ * This state machine manages all showing and hiding of the attachment
+ * notification bar. It is only called if any change happened so that
+ * recalculating of the notification is needed:
+ * - keywords changed
+ * - manual reminder was toggled
+ * - attachments changed
+ * - manual reminder is disabled
+ *
+ * It does not track whether the notification is still up when it should be.
+ * That allows the user to close it any time without this function showing
+ * it again.
+ * We ensure notification is only shown on right events, e.g. only when we have
+ * keywords and attachments were removed (but not when we have keywords and
+ * manual reminder was just turned off). We always show the notification
+ * again if keywords change (if no attachments and no manual reminder).
+ *
+ * @param aForce If set to true, notification will be shown immediately if
+ * there are any keywords. If set to false, it is shown only when
+ * they have changed.
+ */
+function manageAttachmentNotification(aForce = false) {
+ let keywords;
+ let keywordsCount = 0;
+
+ // First see if the notification is to be hidden due to reasons other than
+ // not having keywords.
+ let removeNotification = attachmentNotificationSupressed();
+
+ // If that is not true, we need to look at the state of keywords.
+ if (!removeNotification) {
+ if (attachmentWorker.lastMessage) {
+ // We know the state of keywords, so process them.
+ if (attachmentWorker.lastMessage.length) {
+ keywords = attachmentWorker.lastMessage.join(", ");
+ keywordsCount = attachmentWorker.lastMessage.length;
+ }
+ removeNotification = keywordsCount == 0;
+ } else {
+ // We don't know keywords, so get them first.
+ // If aForce was true, and some keywords are found, we get to run again from
+ // attachmentWorker.onmessage().
+ gAttachmentNotifier.redetectKeywords(aForce);
+ return;
+ }
+ }
+
+ let notification =
+ gComposeNotification.getNotificationWithValue("attachmentReminder");
+ if (removeNotification) {
+ if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+ return;
+ }
+
+ // We have some keywords, however only pop up the notification if requested
+ // to do so.
+ if (!aForce) {
+ return;
+ }
+
+ let textValue = getComposeBundle().getString(
+ "attachmentReminderKeywordsMsgs"
+ );
+ textValue = PluralForm.get(keywordsCount, textValue).replace(
+ "#1",
+ keywordsCount
+ );
+ // If the notification already exists, we simply add the new attachment
+ // specific keywords to the existing notification instead of creating it
+ // from scratch.
+ if (notification) {
+ let msgContainer = notification.messageText.querySelector(
+ "#attachmentReminderText"
+ );
+ msgContainer.textContent = textValue;
+ let keywordsContainer = notification.messageText.querySelector(
+ "#attachmentKeywords"
+ );
+ keywordsContainer.textContent = keywords;
+ return;
+ }
+
+ // Construct the notification as we don't have one.
+ let msg = document.createElement("div");
+ msg.onclick = function (event) {
+ openOptionsDialog("paneCompose", "compositionAttachmentsCategory", {
+ subdialog: "attachment_reminder_button",
+ });
+ };
+
+ let msgText = document.createElement("span");
+ msg.appendChild(msgText);
+ msgText.id = "attachmentReminderText";
+ msgText.textContent = textValue;
+ let msgKeywords = document.createElement("span");
+ msg.appendChild(msgKeywords);
+ msgKeywords.id = "attachmentKeywords";
+ msgKeywords.textContent = keywords;
+ let addButton = {
+ "l10n-id": "add-attachment-notification-reminder2",
+ callback(aNotificationBar, aButton) {
+ goDoCommand("cmd_attachFile");
+ return true; // keep notification open (the state machine will decide on it later)
+ },
+ };
+
+ let remindLaterMenuPopup = document.createXULElement("menupopup");
+ remindLaterMenuPopup.id = "reminderBarPopup";
+ let disableAttachmentReminder = document.createXULElement("menuitem");
+ disableAttachmentReminder.id = "disableReminder";
+ disableAttachmentReminder.setAttribute(
+ "label",
+ getComposeBundle().getString("disableAttachmentReminderButton")
+ );
+ disableAttachmentReminder.addEventListener("command", event => {
+ gDisableAttachmentReminder = true;
+ toggleAttachmentReminder(false);
+ event.stopPropagation();
+ });
+ remindLaterMenuPopup.appendChild(disableAttachmentReminder);
+
+ // The notification code only deals with buttons but we need a toolbarbutton,
+ // so we construct it and add it ourselves.
+ let remindButton = document.createXULElement("toolbarbutton", {
+ is: "toolbarbutton-menu-button",
+ });
+ remindButton.classList.add("notification-button", "small-button");
+ remindButton.setAttribute(
+ "accessKey",
+ getComposeBundle().getString("remindLaterButton.accesskey")
+ );
+ remindButton.setAttribute(
+ "label",
+ getComposeBundle().getString("remindLaterButton")
+ );
+ remindButton.addEventListener("command", function (event) {
+ toggleAttachmentReminder(true);
+ });
+ remindButton.appendChild(remindLaterMenuPopup);
+
+ notification = gComposeNotification.appendNotification(
+ "attachmentReminder",
+ {
+ label: "",
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ [addButton]
+ );
+ notification.setAttribute("id", "attachmentNotificationBox");
+
+ notification.messageText.appendChild(msg);
+ notification.buttonContainer.appendChild(remindButton);
+}
+
+function clearRecipPillKeyIssues() {
+ for (let pill of document.querySelectorAll("mail-address-pill.key-issue")) {
+ pill.classList.remove("key-issue");
+ }
+}
+
+/**
+ * @returns {string[]} - All current recipient email addresses, lowercase.
+ */
+function getEncryptionCompatibleRecipients() {
+ let recipientPills = [
+ ...document.querySelectorAll(
+ "#toAddrContainer > mail-address-pill, #ccAddrContainer > mail-address-pill, #bccAddrContainer > mail-address-pill"
+ ),
+ ];
+ let recipients = [
+ ...new Set(recipientPills.map(pill => pill.emailAddress.toLowerCase())),
+ ];
+ return recipients;
+}
+
+const PRErrorCodeSuccess = 0;
+const certificateUsageEmailRecipient = 0x0020;
+
+var gEmailsWithMissingKeys = null;
+var gEmailsWithMissingCerts = null;
+
+/**
+ * @returns {boolean} true if checking openpgp keys is necessary
+ */
+function mustCheckRecipientKeys() {
+ let remindOpenPGP = Services.prefs.getBoolPref(
+ "mail.openpgp.remind_encryption_possible"
+ );
+
+ let autoEnablePref = Services.prefs.getBoolPref(
+ "mail.e2ee.auto_enable",
+ false
+ );
+
+ return (
+ isPgpConfigured() && (gSendEncrypted || remindOpenPGP || autoEnablePref)
+ );
+}
+
+/**
+ * Check available OpenPGP public encryption keys for the given email
+ * addresses. (This function assumes the caller has already called
+ * mustCheckRecipientKeys() and the result was true.)
+ *
+ * gEmailsWithMissingKeys will be set to an array of email addresses
+ * (a subset of the input) that do NOT have a usable
+ * (valid + accepted) key.
+ *
+ * @param {string[]} recipients - The addresses to lookup.
+ */
+async function checkRecipientKeys(recipients) {
+ gEmailsWithMissingKeys = [];
+
+ for (let addr of recipients) {
+ let keyMetas = await EnigmailKeyRing.getEncryptionKeyMeta(addr);
+
+ if (keyMetas.length == 1 && keyMetas[0].readiness == "alias") {
+ // Skip if this is an alias email.
+ continue;
+ }
+
+ if (!keyMetas.some(k => k.readiness == "accepted")) {
+ gEmailsWithMissingKeys.push(addr);
+ continue;
+ }
+ }
+}
+
+/**
+ * @returns {boolean} true if checking s/mime certificates is necessary
+ */
+function mustCheckRecipientCerts() {
+ let remindSMime = Services.prefs.getBoolPref(
+ "mail.smime.remind_encryption_possible"
+ );
+
+ let autoEnablePref = Services.prefs.getBoolPref(
+ "mail.e2ee.auto_enable",
+ false
+ );
+
+ return (
+ isSmimeEncryptionConfigured() &&
+ (gSendEncrypted || remindSMime || autoEnablePref)
+ );
+}
+
+/**
+ * Check available S/MIME encryption certificates for the given email
+ * addresses. (This function assumes the caller has already called
+ * mustCheckRecipientCerts() and the result was true.)
+ *
+ * gEmailsWithMissingCerts will be set to an array of email addresses
+ * (a subset of the input) that do NOT have a usable (valid) certificate.
+ *
+ * This function might take significant time to complete, because
+ * certificate verification involves OCSP, which runs on a background
+ * thread.
+ *
+ * @param {string[]} recipients - The addresses to lookup.
+ */
+function checkRecipientCerts(recipients) {
+ return new Promise((resolve, reject) => {
+ if (gSMPendingCertLookupSet.size) {
+ reject(
+ new Error(
+ "Must not be called while previous checks are still in progress"
+ )
+ );
+ }
+
+ gEmailsWithMissingCerts = [];
+
+ function continueCheckRecipientCerts() {
+ gEmailsWithMissingCerts = recipients.filter(
+ email => !gSMFields.haveValidCertForEmail(email)
+ );
+ resolve();
+ }
+
+ /** @implements {nsIDoneFindCertForEmailCallback} */
+ let doneFindCertForEmailCallback = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIDoneFindCertForEmailCallback",
+ ]),
+
+ findCertDone(email, cert) {
+ let isStaleResult = !gSMPendingCertLookupSet.has(email);
+ // isStaleResult true means, this recipient was removed by the
+ // user while we were looking for the cert in the background.
+ // Let's remember the result, but don't trigger any actions
+ // based on it.
+
+ if (cert) {
+ gSMFields.cacheValidCertForEmail(email, cert ? cert.dbKey : "");
+ }
+ if (isStaleResult) {
+ return;
+ }
+ gSMPendingCertLookupSet.delete(email);
+ if (!cert && !gSMCertsAlreadyLookedUpInLDAP.has(email)) {
+ let autocompleteLdap = Services.prefs.getBoolPref(
+ "ldap_2.autoComplete.useDirectory"
+ );
+
+ if (autocompleteLdap) {
+ gSMCertsAlreadyLookedUpInLDAP.add(email);
+
+ let autocompleteDirectory = null;
+ if (gCurrentIdentity.overrideGlobalPref) {
+ autocompleteDirectory = gCurrentIdentity.directoryServer;
+ } else {
+ autocompleteDirectory = Services.prefs.getCharPref(
+ "ldap_2.autoComplete.directoryServer"
+ );
+ }
+
+ if (autocompleteDirectory) {
+ window.openDialog(
+ "chrome://messenger-smime/content/certFetchingStatus.xhtml",
+ "",
+ "chrome,resizable=1,modal=1,dialog=1",
+ autocompleteDirectory,
+ [email]
+ );
+ }
+
+ gSMPendingCertLookupSet.add(email);
+ gSMFields.asyncFindCertByEmailAddr(
+ email,
+ doneFindCertForEmailCallback
+ );
+ }
+ }
+
+ if (gSMPendingCertLookupSet.size) {
+ // must continue to wait for more queued lookups to complete
+ return;
+ }
+
+ // No more lookups pending.
+ continueCheckRecipientCerts();
+ },
+ };
+
+ for (let email of recipients) {
+ if (gSMFields.haveValidCertForEmail(email)) {
+ continue;
+ }
+
+ if (gSMPendingCertLookupSet.has(email)) {
+ throw new Error(`cert lookup still pending for ${email}`);
+ }
+
+ gSMPendingCertLookupSet.add(email);
+ gSMFields.asyncFindCertByEmailAddr(email, doneFindCertForEmailCallback);
+ }
+
+ // If we haven't queued any lookups, we continue immediately
+ if (!gSMPendingCertLookupSet.size) {
+ continueCheckRecipientCerts();
+ }
+ });
+}
+
+/**
+ * gCheckEncryptionStateCompletionIsPending means that async work
+ * started by checkEncryptionState() has not yet completed.
+ */
+var gCheckEncryptionStateCompletionIsPending = false;
+
+/**
+ * gCheckEncryptionStateNeedsRestart means that checkEncryptionState()
+ * was called, while its async operations were still running.
+ * The additional to checkEncryptionState() was treated as a no-op,
+ * but gCheckEncryptionStateNeedsRestart was set to true, to remember
+ * that checkEncryptionState() must be immediately restarted after its
+ * previous execution is done. This will the restarted
+ * checkEncryptionState() execution to detect and handle changes that
+ * could result in a different state.
+ */
+var gCheckEncryptionStateNeedsRestart = false;
+
+/**
+ * gWasCESTriggeredByComposerChange is used to track whether an
+ * encryption-state-checked event should be sent after an ongoing
+ * execution of checkEncryptionState() is done.
+ * The purpose of the encryption-state-checked event is to allow our
+ * automated tests to be notified as soon as an automatic call to
+ * checkEncryptionState() (and all related async calls) is complete,
+ * which means all automatic adjustments to the global encryption state
+ * are done, and the automated test code may proceed to compare the
+ * state to our exptectations.
+ * We want that event to be sent after modifications were made to the
+ * composer window itself, such as sender identity and recipients.
+ * However, we want to ignore calls to checkEncryptionState() that
+ * were triggered indirectly after OpenPGP keys were changed.
+ * If an event was originally triggered by a change to OpenPGP keys,
+ * and the async processing of checkEncryptionState() was still running,
+ * and another direct change to the composer window was made, which
+ * shall result in sending a encryption-state-checked after completion,
+ * then the flag gWasCESTriggeredByComposerChange will be set,
+ * which will cause the event to be sent after the restarted call
+ * to checkEncryptionState() is complete.
+ */
+var gWasCESTriggeredByComposerChange = false;
+
+/**
+ * Perform all checks that are necessary to update the state of
+ * email encryption, based on the current recipients. This should be
+ * done whenever the recipient list or the status of available keys/certs
+ * has changed. All automatic actions for encryption related settings
+ * will be triggered accordingly.
+ * This function will trigger async activity, and the resulting actions
+ * (e.g. update of UI elements) may happen after a delay.
+ * It's safe to call this while processing hasn't completed yet, in this
+ * scenario the processing will be restarted, once pending
+ * activity has completed.
+ *
+ * @param {string} [trigger] - A string that gives information about
+ * the reason why this function is being called.
+ * This parameter is intended to help with automated testing.
+ * If the trigger string starts with "openpgp-" then no completition
+ * event will be dispatched. This allows the automated test code to
+ * wait for events that are directly related to properties of the
+ * composer window, only.
+ */
+async function checkEncryptionState(trigger) {
+ if (!gLoadingComplete) {
+ // Let's not do this while we're still loading the composer window,
+ // it can have side effects, see bug 1777683.
+ // Also, if multiple recipients are added to an email automatically
+ // e.g. during reply-all, it doesn't make sense to execute this
+ // function every time after one of them gets added.
+ return;
+ }
+
+ if (!/^openpgp-/.test(trigger)) {
+ gWasCESTriggeredByComposerChange = true;
+ }
+
+ if (gCheckEncryptionStateCompletionIsPending) {
+ // avoid concurrency
+ gCheckEncryptionStateNeedsRestart = true;
+ return;
+ }
+
+ let remindSMime = Services.prefs.getBoolPref(
+ "mail.smime.remind_encryption_possible"
+ );
+ let remindOpenPGP = Services.prefs.getBoolPref(
+ "mail.openpgp.remind_encryption_possible"
+ );
+ let autoEnablePref = Services.prefs.getBoolPref(
+ "mail.e2ee.auto_enable",
+ false
+ );
+
+ if (!gSendEncrypted && !autoEnablePref && !remindSMime && !remindOpenPGP) {
+ // No need to check.
+ updateEncryptionDependencies();
+ updateKeyCertNotifications([]);
+ updateEncryptionTechReminder(null);
+ if (gWasCESTriggeredByComposerChange) {
+ document.dispatchEvent(new CustomEvent("encryption-state-checked"));
+ gWasCESTriggeredByComposerChange = false;
+ }
+ return;
+ }
+
+ let recipients = getEncryptionCompatibleRecipients();
+ let checkingCerts = mustCheckRecipientCerts();
+ let checkingKeys = mustCheckRecipientKeys();
+
+ async function continueCheckEncryptionStateSub() {
+ let canEncryptSMIME =
+ recipients.length && checkingCerts && !gEmailsWithMissingCerts.length;
+ let canEncryptOpenPGP =
+ recipients.length && checkingKeys && !gEmailsWithMissingKeys.length;
+
+ let autoEnabledJustNow = false;
+
+ if (
+ gSendEncrypted &&
+ gUserTouchedSendEncrypted &&
+ !isPgpConfigured() &&
+ !isSmimeEncryptionConfigured()
+ ) {
+ notifyIdentityCannotEncrypt(true, gCurrentIdentity.email);
+ } else {
+ notifyIdentityCannotEncrypt(false, gCurrentIdentity.email);
+ }
+
+ if (
+ !gSendEncrypted &&
+ autoEnablePref &&
+ !gUserTouchedSendEncrypted &&
+ recipients.length &&
+ (canEncryptSMIME || canEncryptOpenPGP)
+ ) {
+ if (!canEncryptSMIME) {
+ gSelectedTechnologyIsPGP = true;
+ } else if (!canEncryptOpenPGP) {
+ gSelectedTechnologyIsPGP = false;
+ }
+ gSendEncrypted = true;
+ autoEnabledJustNow = true;
+ removeAutoDisableNotification();
+ }
+
+ if (
+ !gIsRelatedToEncryptedOriginal &&
+ !autoEnabledJustNow &&
+ !gUserTouchedSendEncrypted &&
+ gSendEncrypted &&
+ !canEncryptSMIME &&
+ !canEncryptOpenPGP
+ ) {
+ // The auto_disable pref is ignored if auto_enable is false
+ let autoDisablePref = Services.prefs.getBoolPref(
+ "mail.e2ee.auto_disable",
+ false
+ );
+ if (autoEnablePref && autoDisablePref && !gUserTouchedSendEncrypted) {
+ gSendEncrypted = false;
+ let notifyPref = Services.prefs.getBoolPref(
+ "mail.e2ee.notify_on_auto_disable",
+ true
+ );
+ if (notifyPref) {
+ // Most likely the notification is not showing yet, and we
+ // must append it. (We should have removed an existing
+ // notification at the time encryption was enabled.)
+ // However, double check to avoid that we'll show it twice.
+ const NOTIFICATION_NAME = "e2eeDisableNotification";
+ let notification =
+ gComposeNotification.getNotificationWithValue(NOTIFICATION_NAME);
+ if (!notification) {
+ gComposeNotification.appendNotification(
+ NOTIFICATION_NAME,
+ {
+ label: { "l10n-id": "auto-disable-e2ee-warning" },
+ priority: gComposeNotification.PRIORITY_WARNING_LOW,
+ },
+ []
+ );
+ }
+ }
+ }
+ }
+
+ let techPref = gCurrentIdentity.getIntAttribute("e2etechpref");
+
+ if (gSendEncrypted && canEncryptSMIME && canEncryptOpenPGP) {
+ // No change if 0
+ if (techPref == 1) {
+ gSelectedTechnologyIsPGP = false;
+ } else if (techPref == 2) {
+ gSelectedTechnologyIsPGP = true;
+ }
+ }
+
+ if (
+ gSendEncrypted &&
+ canEncryptSMIME &&
+ !canEncryptOpenPGP &&
+ gSelectedTechnologyIsPGP
+ ) {
+ gSelectedTechnologyIsPGP = false;
+ }
+
+ if (
+ gSendEncrypted &&
+ !canEncryptSMIME &&
+ canEncryptOpenPGP &&
+ !gSelectedTechnologyIsPGP
+ ) {
+ gSelectedTechnologyIsPGP = true;
+ }
+
+ updateEncryptionDependencies();
+
+ if (!gSendEncrypted) {
+ updateKeyCertNotifications([]);
+ if (recipients.length && (canEncryptSMIME || canEncryptOpenPGP)) {
+ let useTech;
+ if (canEncryptSMIME && canEncryptOpenPGP) {
+ if (techPref == 1) {
+ useTech = "SMIME";
+ } else {
+ useTech = "OpenPGP";
+ }
+ } else {
+ useTech = canEncryptOpenPGP ? "OpenPGP" : "SMIME";
+ }
+ updateEncryptionTechReminder(useTech);
+ } else {
+ updateEncryptionTechReminder(null);
+ }
+ } else {
+ updateKeyCertNotifications(
+ gSelectedTechnologyIsPGP
+ ? gEmailsWithMissingKeys
+ : gEmailsWithMissingCerts
+ );
+ updateEncryptionTechReminder(null);
+ }
+
+ gCheckEncryptionStateCompletionIsPending = false;
+
+ if (gCheckEncryptionStateNeedsRestart) {
+ // Recursive call, which is acceptable (and not blocking),
+ // because necessary long actions will be triggered asynchronously.
+ gCheckEncryptionStateNeedsRestart = false;
+ await checkEncryptionState(trigger);
+ } else if (gWasCESTriggeredByComposerChange) {
+ document.dispatchEvent(new CustomEvent("encryption-state-checked"));
+ gWasCESTriggeredByComposerChange = false;
+ }
+ }
+
+ let pendingPromises = [];
+
+ if (checkingCerts) {
+ pendingPromises.push(checkRecipientCerts(recipients));
+ }
+
+ if (checkingKeys) {
+ pendingPromises.push(checkRecipientKeys(recipients));
+ }
+
+ gCheckEncryptionStateNeedsRestart = false;
+ gCheckEncryptionStateCompletionIsPending = true;
+
+ Promise.all(pendingPromises).then(continueCheckEncryptionStateSub);
+}
+
+/**
+ * Display (or hide) the notification that informs the user that
+ * encryption is possible (but currently not enabled).
+ *
+ * @param {string} technology - The technology that is possible,
+ * ("OpenPGP" or "SMIME"), or null if none is possible.
+ */
+function updateEncryptionTechReminder(technology) {
+ let enableNotification =
+ gComposeNotification.getNotificationWithValue("enableNotification");
+ if (enableNotification) {
+ gComposeNotification.removeNotification(enableNotification);
+ }
+
+ if (!technology || (technology != "OpenPGP" && technology != "SMIME")) {
+ return;
+ }
+
+ let labelId =
+ technology == "OpenPGP"
+ ? "can-encrypt-openpgp-notification"
+ : "can-encrypt-smime-notification";
+
+ gComposeNotification.appendNotification(
+ "enableNotification",
+ {
+ label: { "l10n-id": labelId },
+ priority: gComposeNotification.PRIORITY_INFO_LOW,
+ },
+ [
+ {
+ "l10n-id": "can-e2e-encrypt-button",
+ callback() {
+ gSelectedTechnologyIsPGP = technology == "OpenPGP";
+ gSendEncrypted = true;
+ gUserTouchedSendEncrypted = true;
+ checkEncryptionState();
+ return true;
+ },
+ },
+ ]
+ );
+}
+
+/**
+ * Display (or hide) the notification that informs the user that
+ * encryption isn't possible, because the currently selected Sender
+ * (From) identity isn't configured for end-to-end-encryption.
+ *
+ * @param {boolean} show - Show if true, hide if false.
+ * @param {string} addr - email address to show in notification
+ */
+async function notifyIdentityCannotEncrypt(show, addr) {
+ const NOTIFICATION_NAME = "IdentityCannotEncrypt";
+
+ let notification =
+ gComposeNotification.getNotificationWithValue(NOTIFICATION_NAME);
+
+ if (show) {
+ if (!notification) {
+ gComposeNotification.appendNotification(
+ NOTIFICATION_NAME,
+ {
+ label: await document.l10n.formatValue(
+ "openpgp-key-issue-notification-from",
+ {
+ addr,
+ }
+ ),
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ []
+ );
+ }
+ } else if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+}
+
+/**
+ * Show an appropriate notification based on the given list of
+ * email addresses that cannot be used with email encryption
+ * (because of missing usable OpenPGP public keys or S/MIME certs).
+ * The list may be empty, which means no notification will be shown
+ * (or existing notifications will be removed).
+ *
+ * @param {string[]} emailsWithMissing - The email addresses that prevent
+ * using encryption, because certs/keys are missing.
+ */
+function updateKeyCertNotifications(emailsWithMissing) {
+ const NOTIFICATION_NAME = "keyNotification";
+
+ let notification =
+ gComposeNotification.getNotificationWithValue(NOTIFICATION_NAME);
+ if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+
+ // Always refresh the pills UI.
+ clearRecipPillKeyIssues();
+
+ // Interrupt if we don't have any issue.
+ if (!emailsWithMissing.length) {
+ return;
+ }
+
+ // Update recipient pills.
+ for (let pill of document.querySelectorAll("mail-address-pill")) {
+ if (
+ emailsWithMissing.includes(pill.emailAddress.toLowerCase()) &&
+ !pill.classList.contains("invalid-address")
+ ) {
+ pill.classList.add("key-issue");
+ }
+ }
+
+ /**
+ * Display the new key notification.
+ */
+ let buttons = [];
+ buttons.push({
+ "l10n-id": "key-notification-disable-encryption",
+ callback() {
+ gUserTouchedSendEncrypted = true;
+ gSendEncrypted = false;
+ checkEncryptionState();
+ return true;
+ },
+ });
+
+ if (gSelectedTechnologyIsPGP) {
+ buttons.push({
+ "l10n-id": "key-notification-resolve",
+ callback() {
+ showMessageComposeSecurityStatus();
+ return true;
+ },
+ });
+ }
+
+ let label;
+
+ if (emailsWithMissing.length == 1) {
+ let id = gSelectedTechnologyIsPGP
+ ? "openpgp-key-issue-notification-single"
+ : "smime-cert-issue-notification-single";
+ label = {
+ "l10n-id": id,
+ "l10n-args": { addr: emailsWithMissing[0] },
+ };
+ } else {
+ let id = gSelectedTechnologyIsPGP
+ ? "openpgp-key-issue-notification-multi"
+ : "smime-cert-issue-notification-multi";
+
+ label = {
+ "l10n-id": id,
+ "l10n-args": { count: emailsWithMissing.length },
+ };
+ }
+
+ gComposeNotification.appendNotification(
+ NOTIFICATION_NAME,
+ {
+ label,
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ buttons
+ );
+}
+
+/**
+ * Returns whether the attachment notification should be suppressed regardless
+ * of the state of keywords.
+ */
+function attachmentNotificationSupressed() {
+ return (
+ gDisableAttachmentReminder ||
+ gManualAttachmentReminder ||
+ gAttachmentBucket.getRowCount()
+ );
+}
+
+var attachmentWorker = new Worker("resource:///modules/AttachmentChecker.jsm");
+
+// The array of currently found keywords. Or null if keyword detection wasn't
+// run yet so we don't know.
+attachmentWorker.lastMessage = null;
+
+attachmentWorker.onerror = function (error) {
+ console.error("Attachment Notification Worker error!!! " + error.message);
+ throw error;
+};
+
+/**
+ * Called when attachmentWorker finishes checking of the message for keywords.
+ *
+ * @param event If defined, event.data contains an array of found keywords.
+ * @param aManage If set to true and we determine keywords have changed,
+ * manage the notification.
+ * If set to false, just store the new keyword list but do not
+ * touch the notification. That effectively eats the
+ * "keywords changed" event which usually shows the notification
+ * if it was hidden. See manageAttachmentNotification().
+ */
+attachmentWorker.onmessage = function (event, aManage = true) {
+ // Exit if keywords haven't changed.
+ if (
+ !event ||
+ (attachmentWorker.lastMessage &&
+ event.data.toString() == attachmentWorker.lastMessage.toString())
+ ) {
+ return;
+ }
+
+ let data = event ? event.data : [];
+ attachmentWorker.lastMessage = data.slice(0);
+ if (aManage) {
+ manageAttachmentNotification(true);
+ }
+};
+
+/**
+ * Update attachment-related internal flags, UI, and commands.
+ * Called when number of attachments changes.
+ *
+ * @param aShowPane {string} "show": show the attachment pane
+ * "hide": hide the attachment pane
+ * omitted: just update without changing pane visibility
+ * @param aContentChanged {Boolean} optional value to assign to gContentChanged;
+ * defaults to true.
+ */
+function AttachmentsChanged(aShowPane, aContentChanged = true) {
+ gContentChanged = aContentChanged;
+ updateAttachmentPane(aShowPane);
+ manageAttachmentNotification(true);
+ updateAttachmentItems();
+}
+
+/**
+ * This functions returns an array of valid spellcheck languages. It checks
+ * that a dictionary exists for the language passed in, if any. It also
+ * retrieves the corresponding preference and ensures that a dictionary exists.
+ * If not, it adjusts the preference accordingly.
+ * When the nominated dictionary does not exist, the effects are very confusing
+ * to the user: Inline spell checking does not work, although the option is
+ * selected and a spell check dictionary seems to be selected in the options
+ * dialog (the dropdown shows the first list member if the value is not in
+ * the list). It is not at all obvious that the preference value is wrong.
+ * This case can happen two scenarios:
+ * 1) The dictionary that was selected in the preference is removed.
+ * 2) The selected dictionary changes the way it announces itself to the system,
+ * so for example "it_IT" changes to "it-IT" and the previously stored
+ * preference value doesn't apply any more.
+ *
+ * @param {string[]|null} [draftLanguages] - Languages that the message was
+ * composed in.
+ * @returns {string[]}
+ */
+function getValidSpellcheckerDictionaries(draftLanguages) {
+ let prefValue = Services.prefs.getCharPref("spellchecker.dictionary");
+ let spellChecker = Cc["@mozilla.org/spellchecker/engine;1"].getService(
+ Ci.mozISpellCheckingEngine
+ );
+ let dictionaries = Array.from(new Set(prefValue?.split(",")));
+
+ let dictList = spellChecker.getDictionaryList();
+ let count = dictList.length;
+
+ if (count == 0) {
+ // If there are no dictionaries, we can't check the value, so return it.
+ return dictionaries;
+ }
+
+ // Make sure that the draft language contains a valid value.
+ if (
+ draftLanguages &&
+ draftLanguages.every(language => dictList.includes(language))
+ ) {
+ return draftLanguages;
+ }
+
+ // Make sure preference contains a valid value.
+ if (dictionaries.every(language => dictList.includes(language))) {
+ return dictionaries;
+ }
+
+ // Set a valid value, any value will do.
+ Services.prefs.setCharPref("spellchecker.dictionary", dictList[0]);
+ return [dictList[0]];
+}
+
+var dictionaryRemovalObserver = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != "spellcheck-dictionary-remove") {
+ return;
+ }
+ let spellChecker = Cc["@mozilla.org/spellchecker/engine;1"].getService(
+ Ci.mozISpellCheckingEngine
+ );
+
+ let dictList = spellChecker.getDictionaryList();
+ let languages = Array.from(gActiveDictionaries);
+ languages = languages.filter(lang => dictList.includes(lang));
+ if (languages.length === 0) {
+ // Set a valid language from the preference.
+ let prefValue = Services.prefs.getCharPref("spellchecker.dictionary");
+ let prefLanguages = prefValue?.split(",") ?? [];
+ languages = prefLanguages.filter(lang => dictList.includes(lang));
+ if (prefLanguages.length != languages.length && languages.length > 0) {
+ // Fix the preference while we're here. We know it's invalid.
+ Services.prefs.setCharPref(
+ "spellchecker.dictionary",
+ languages.join(",")
+ );
+ }
+ }
+ // Only update the language if we will still be left with any active choice.
+ if (languages.length > 0) {
+ ComposeChangeLanguage(languages);
+ }
+ },
+
+ isAdded: false,
+
+ addObserver() {
+ Services.obs.addObserver(this, "spellcheck-dictionary-remove");
+ this.isAdded = true;
+ },
+
+ removeObserver() {
+ if (this.isAdded) {
+ Services.obs.removeObserver(this, "spellcheck-dictionary-remove");
+ this.isAdded = false;
+ }
+ },
+};
+
+function EditorClick(event) {
+ if (event.target.matches(".remove-card")) {
+ let card = event.target.closest(".moz-card");
+ let url = card.querySelector(".url").href;
+ if (card.matches(".url-replaced")) {
+ card.replaceWith(url);
+ } else {
+ card.remove();
+ }
+ } else if (event.target.matches(`.add-card[data-opened='${gOpened}']`)) {
+ let url = event.target.getAttribute("data-url");
+ let meRect = document.getElementById("messageEditor").getClientRects()[0];
+ let settings = document.getElementById("linkPreviewSettings");
+ let settingsW = 500;
+ settings.style.position = "fixed";
+ settings.style.left =
+ Math.max(settingsW + 20, event.clientX) - settingsW + "px";
+ settings.style.top = meRect.top + event.clientY + 20 + "px";
+ settings.hidden = false;
+ event.target.remove();
+ settings.querySelector(".close").onclick = event => {
+ settings.hidden = true;
+ };
+ settings.querySelector(".preview-replace").onclick = event => {
+ addLinkPreview(url, true);
+ settings.hidden = true;
+ };
+ settings.querySelector(".preview-autoadd").onclick = event => {
+ Services.prefs.setBoolPref(
+ "mail.compose.add_link_preview",
+ event.target.checked
+ );
+ };
+ settings.querySelector(".preview-replace").focus();
+ settings.onkeydown = event => {
+ if (event.key == "Escape") {
+ settings.hidden = true;
+ }
+ };
+ }
+}
+
+/**
+ * Grab Open Graph or Twitter card data from the URL and insert a link preview
+ * into the editor. If no proper data could be found, nothing is inserted.
+ *
+ * @param {string} url - The URL to add preview for.
+ */
+async function addLinkPreview(url) {
+ return fetch(url)
+ .then(response => response.text())
+ .then(text => {
+ let doc = new DOMParser().parseFromString(text, "text/html");
+
+ // If the url has an Open Graph or Twitter card, create a nicer
+ // representation and use that instead.
+ // @see https://ogp.me/
+ // @see https://developer.twitter.com/en/docs/twitter-for-websites/cards/
+ // Also look for standard meta information as a fallback.
+
+ let title =
+ doc
+ .querySelector("meta[property='og:title'],meta[name='twitter:title']")
+ ?.getAttribute("content") ||
+ doc.querySelector("title")?.textContent.trim();
+ let description = doc
+ .querySelector(
+ "meta[property='og:description'],meta[name='twitter:description'],meta[name='description']"
+ )
+ ?.getAttribute("content");
+
+ // Handle the case where we didn't get proper data.
+ if (!title && !description) {
+ console.debug(`No link preview data for url=${url}`);
+ return;
+ }
+
+ let image = doc
+ .querySelector("meta[property='og:image']")
+ ?.getAttribute("content");
+ let alt =
+ doc
+ .querySelector("meta[property='og:image:alt']")
+ ?.getAttribute("content") || "";
+ if (!image) {
+ image = doc
+ .querySelector("meta[name='twitter:image']")
+ ?.getAttribute("content");
+ alt =
+ doc
+ .querySelector("meta[name='twitter:image:alt']")
+ ?.getAttribute("content") || "";
+ }
+ let imgIsTouchIcon = false;
+ if (!image) {
+ image = doc
+ .querySelector(
+ `link[rel='icon']:is(
+ [sizes~='any'],
+ [sizes~='196x196' i],
+ [sizes~='192x192' i]
+ [sizes~='180x180' i],
+ [sizes~='128x128' i]
+ )`
+ )
+ ?.getAttribute("href");
+ alt = "";
+ imgIsTouchIcon = Boolean(image);
+ }
+
+ // Grab our template and fill in the variables.
+ let card = document
+ .getElementById("dataCardTemplate")
+ .content.cloneNode(true).firstElementChild;
+ card.id = "card-" + Date.now();
+ card.querySelector("img").src = image;
+ card.querySelector("img").alt = alt;
+ card.querySelector(".title").textContent = title;
+
+ card.querySelector(".description").textContent = description;
+ card.querySelector(".url").textContent = "🔗 " + url;
+ card.querySelector(".url").href = url;
+ card.querySelector(".url").title = new URL(url).hostname;
+ card.querySelector(".site").textContent = new URL(url).hostname;
+
+ // twitter:card "summary" = Summary Card
+ // twitter:card "summary_large_image" = Summary Card with Large Image
+ if (
+ !imgIsTouchIcon &&
+ (doc.querySelector(
+ "meta[name='twitter:card'][content='summary_large_image']"
+ ) ||
+ doc
+ .querySelector("meta[property='og:image:width']")
+ ?.getAttribute("content") >= 600)
+ ) {
+ card.querySelector("img").style.width = "600px";
+ }
+
+ if (!image) {
+ card.querySelector(".card-pic").remove();
+ }
+
+ // If subject is empty, set that as well.
+ let subject = document.getElementById("msgSubject");
+ if (!subject.value && title) {
+ subject.value = title;
+ }
+
+ // Select the inserted URL so that if the preview is found one can
+ // use undo to remove it and only use the URL instead.
+ // Only do it if there was no typing after the url.
+ let selection = getBrowser().contentDocument.getSelection();
+ let n = selection.focusNode;
+ if (n.textContent.endsWith(url)) {
+ selection.extend(n, n.textContent.lastIndexOf(url));
+ card.classList.add("url-replaced");
+ }
+
+ // Add a line after the card. Otherwise it's hard to continue writing.
+ let line = GetCurrentEditor().returnInParagraphCreatesNewParagraph
+ ? "<p>&#160;</p>"
+ : "<br />";
+ card.classList.add("loading"); // Used for fade-in effect.
+ getBrowser().contentDocument.execCommand(
+ "insertHTML",
+ false,
+ card.outerHTML + line
+ );
+ let cardInDoc = getBrowser().contentDocument.getElementById(card.id);
+ cardInDoc.classList.remove("loading");
+ });
+}
+
+/**
+ * On paste or drop, we may want to modify the content before inserting it into
+ * the editor, replacing file URLs with data URLs when appropriate.
+ */
+function onPasteOrDrop(e) {
+ if (!gMsgCompose.composeHTML) {
+ // We're in the plain text editor. Nothing to do here.
+ return;
+ }
+ gMsgCompose.allowRemoteContent = true;
+
+ // For paste use e.clipboardData, for drop use e.dataTransfer.
+ let dataTransfer = "clipboardData" in e ? e.clipboardData : e.dataTransfer;
+ if (
+ Services.prefs.getBoolPref("mail.compose.add_link_preview", false) &&
+ !Services.io.offline &&
+ !dataTransfer.types.includes("text/html")
+ ) {
+ let type = dataTransfer.types.find(t =>
+ ["text/uri-list", "text/x-moz-url", "text/plain"].includes(t)
+ );
+ if (type) {
+ let url = dataTransfer.getData(type).split("\n")[0].trim();
+ if (/^https?:\/\/\S+$/.test(url)) {
+ e.preventDefault(); // We'll handle the pasting manually.
+ getBrowser().contentDocument.execCommand("insertHTML", false, url);
+ addLinkPreview(url);
+ return;
+ }
+ }
+ }
+
+ if (!dataTransfer.types.includes("text/html")) {
+ return;
+ }
+
+ // Ok, we have html content to paste.
+ let html = dataTransfer.getData("text/html");
+ let doc = new DOMParser().parseFromString(html, "text/html");
+ let tmpD = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ let pendingConversions = 0;
+ let needToPreventDefault = true;
+ for (let img of doc.images) {
+ if (!/^file:/i.test(img.src)) {
+ // Doesn't start with file:. Nothing to do here.
+ continue;
+ }
+
+ // This may throw if the URL is invalid for the OS.
+ let nsFile;
+ try {
+ nsFile = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getFileFromURLSpec(img.src);
+ } catch (ex) {
+ continue;
+ }
+
+ if (!nsFile.exists()) {
+ continue;
+ }
+
+ if (!tmpD.contains(nsFile)) {
+ // Not anywhere under the temp dir.
+ continue;
+ }
+
+ let contentType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromFile(nsFile);
+ if (!contentType.startsWith("image/")) {
+ continue;
+ }
+
+ // If we ever get here, we need to prevent the default paste or drop since
+ // the code below will do its own insertion.
+ if (needToPreventDefault) {
+ e.preventDefault();
+ needToPreventDefault = false;
+ }
+
+ File.createFromNsIFile(nsFile).then(function (file) {
+ if (file.lastModified < Date.now() - 60000) {
+ // Not put in temp in the last minute. May be something other than
+ // a copy-paste. Let's not allow that.
+ return;
+ }
+
+ let doTheInsert = function () {
+ // Now run it through sanitation to make sure there wasn't any
+ // unwanted things in the content.
+ let ParserUtils = Cc["@mozilla.org/parserutils;1"].getService(
+ Ci.nsIParserUtils
+ );
+ let html2 = ParserUtils.sanitize(
+ doc.documentElement.innerHTML,
+ ParserUtils.SanitizerAllowStyle
+ );
+ getBrowser().contentDocument.execCommand("insertHTML", false, html2);
+ };
+
+ // Everything checks out. Convert file to data URL.
+ let reader = new FileReader();
+ reader.addEventListener("load", function () {
+ let dataURL = reader.result;
+ pendingConversions--;
+ img.src = dataURL;
+ if (pendingConversions == 0) {
+ doTheInsert();
+ }
+ });
+ reader.addEventListener("error", function () {
+ pendingConversions--;
+ if (pendingConversions == 0) {
+ doTheInsert();
+ }
+ });
+
+ pendingConversions++;
+ reader.readAsDataURL(file);
+ });
+ }
+}
+
+/* eslint-disable complexity */
+async function ComposeStartup() {
+ // Findbar overlay
+ if (!document.getElementById("findbar-replaceButton")) {
+ let replaceButton = document.createXULElement("toolbarbutton");
+ replaceButton.setAttribute("id", "findbar-replaceButton");
+ replaceButton.setAttribute("class", "toolbarbutton-1 tabbable");
+ replaceButton.setAttribute(
+ "label",
+ getComposeBundle().getString("replaceButton.label")
+ );
+ replaceButton.setAttribute(
+ "accesskey",
+ getComposeBundle().getString("replaceButton.accesskey")
+ );
+ replaceButton.setAttribute(
+ "tooltiptext",
+ getComposeBundle().getString("replaceButton.tooltip")
+ );
+ replaceButton.setAttribute("oncommand", "findbarFindReplace();");
+
+ let findbar = document.getElementById("FindToolbar");
+ let lastButton = findbar.getElement("find-entire-word");
+ let tSeparator = document.createXULElement("toolbarseparator");
+ tSeparator.setAttribute("id", "findbar-beforeReplaceSeparator");
+ lastButton.parentNode.insertBefore(
+ replaceButton,
+ lastButton.nextElementSibling
+ );
+ lastButton.parentNode.insertBefore(
+ tSeparator,
+ lastButton.nextElementSibling
+ );
+ }
+
+ var params = null; // New way to pass parameters to the compose window as a nsIMsgComposeParameters object
+ var args = null; // old way, parameters are passed as a string
+ gBodyFromArgs = false;
+
+ if (window.arguments && window.arguments[0]) {
+ try {
+ if (window.arguments[0] instanceof Ci.nsIMsgComposeParams) {
+ params = window.arguments[0];
+ gBodyFromArgs = params.composeFields && params.composeFields.body;
+ } else {
+ params = handleMailtoArgs(window.arguments[0]);
+ }
+ } catch (ex) {
+ dump("ERROR with parameters: " + ex + "\n");
+ }
+
+ // if still no dice, try and see if the params is an old fashioned list of string attributes
+ // XXX can we get rid of this yet?
+ if (!params) {
+ args = GetArgs(window.arguments[0]);
+ }
+ }
+
+ // Set a sane starting width/height for all resolutions on new profiles.
+ // Do this before the window loads.
+ if (!document.documentElement.hasAttribute("width")) {
+ // Prefer 860x800.
+ let defaultHeight = Math.min(screen.availHeight, 800);
+ let defaultWidth = Math.min(screen.availWidth, 860);
+
+ // On small screens, default to maximized state.
+ if (defaultHeight <= 600) {
+ document.documentElement.setAttribute("sizemode", "maximized");
+ }
+
+ document.documentElement.setAttribute("width", defaultWidth);
+ document.documentElement.setAttribute("height", defaultHeight);
+ // Make sure we're safe at the left/top edge of screen
+ document.documentElement.setAttribute("screenX", screen.availLeft);
+ document.documentElement.setAttribute("screenY", screen.availTop);
+ }
+
+ // Observe dictionary removals.
+ dictionaryRemovalObserver.addObserver();
+
+ let messageEditor = document.getElementById("messageEditor");
+ messageEditor.addEventListener("paste", onPasteOrDrop);
+ messageEditor.addEventListener("drop", onPasteOrDrop);
+
+ let identityList = document.getElementById("msgIdentity");
+ if (identityList) {
+ FillIdentityList(identityList);
+ }
+
+ if (!params) {
+ // This code will go away soon as now arguments are passed to the window using a object of type nsMsgComposeParams instead of a string
+
+ params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(
+ Ci.nsIMsgComposeParams
+ );
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ if (args) {
+ // Convert old fashion arguments into params
+ var composeFields = params.composeFields;
+ if (args.bodyislink == "true") {
+ params.bodyIsLink = true;
+ }
+ if (args.type) {
+ params.type = args.type;
+ }
+ if (args.format) {
+ // Only use valid values.
+ if (
+ args.format == Ci.nsIMsgCompFormat.PlainText ||
+ args.format == Ci.nsIMsgCompFormat.HTML ||
+ args.format == Ci.nsIMsgCompFormat.OppositeOfDefault
+ ) {
+ params.format = args.format;
+ } else if (args.format.toLowerCase().trim() == "html") {
+ params.format = Ci.nsIMsgCompFormat.HTML;
+ } else if (args.format.toLowerCase().trim() == "text") {
+ params.format = Ci.nsIMsgCompFormat.PlainText;
+ }
+ }
+ if (args.originalMsgURI) {
+ params.originalMsgURI = args.originalMsgURI;
+ }
+ if (args.preselectid) {
+ params.identity = MailServices.accounts.getIdentity(args.preselectid);
+ }
+ if (args.from) {
+ composeFields.from = args.from;
+ }
+ if (args.to) {
+ composeFields.to = args.to;
+ }
+ if (args.cc) {
+ composeFields.cc = args.cc;
+ }
+ if (args.bcc) {
+ composeFields.bcc = args.bcc;
+ }
+ if (args.newsgroups) {
+ composeFields.newsgroups = args.newsgroups;
+ }
+ if (args.subject) {
+ composeFields.subject = args.subject;
+ }
+ if (args.attachment && window.arguments[1] instanceof Ci.nsICommandLine) {
+ let attachmentList = args.attachment.split(",");
+ for (let attachmentName of attachmentList) {
+ // resolveURI does all the magic around working out what the
+ // attachment is, including web pages, and generating the correct uri.
+ let uri = window.arguments[1].resolveURI(attachmentName);
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ // If uri is for a file and it exists set the attachment size.
+ if (uri instanceof Ci.nsIFileURL) {
+ if (uri.file.exists()) {
+ attachment.size = uri.file.fileSize;
+ } else {
+ attachment = null;
+ }
+ }
+
+ // Only want to attach if a file that exists or it is not a file.
+ if (attachment) {
+ attachment.url = uri.spec;
+ composeFields.addAttachment(attachment);
+ } else {
+ let title = getComposeBundle().getString("errorFileAttachTitle");
+ let msg = getComposeBundle().getFormattedString(
+ "errorFileAttachMessage",
+ [attachmentName]
+ );
+ Services.prompt.alert(null, title, msg);
+ }
+ }
+ }
+ if (args.newshost) {
+ composeFields.newshost = args.newshost;
+ }
+ if (args.message) {
+ let msgFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ if (PathUtils.parent(args.message) == ".") {
+ let workingDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ args.message = PathUtils.join(
+ workingDir.path,
+ PathUtils.filename(args.message)
+ );
+ }
+ msgFile.initWithPath(args.message);
+
+ if (!msgFile.exists()) {
+ let title = getComposeBundle().getString("errorFileMessageTitle");
+ let msg = getComposeBundle().getFormattedString(
+ "errorFileMessageMessage",
+ [args.message]
+ );
+ Services.prompt.alert(null, title, msg);
+ } else {
+ let data = "";
+ let fstream = null;
+ let cstream = null;
+
+ try {
+ fstream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ cstream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ fstream.init(msgFile, -1, 0, 0); // Open file in default/read-only mode.
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let str = {};
+ let read = 0;
+
+ do {
+ // Read as much as we can and put it in str.value.
+ read = cstream.readString(0xffffffff, str);
+ data += str.value;
+ } while (read != 0);
+ } catch (e) {
+ let title = getComposeBundle().getString("errorFileMessageTitle");
+ let msg = getComposeBundle().getFormattedString(
+ "errorLoadFileMessageMessage",
+ [args.message]
+ );
+ Services.prompt.alert(null, title, msg);
+ } finally {
+ if (cstream) {
+ cstream.close();
+ }
+ if (fstream) {
+ fstream.close();
+ }
+ }
+
+ if (data) {
+ let pos = data.search(/\S/); // Find first non-whitespace character.
+
+ if (
+ params.format != Ci.nsIMsgCompFormat.PlainText &&
+ (args.message.endsWith(".htm") ||
+ args.message.endsWith(".html") ||
+ data.substr(pos, 14).toLowerCase() == "<!doctype html" ||
+ data.substr(pos, 5).toLowerCase() == "<html")
+ ) {
+ // We replace line breaks because otherwise they'll be converted to
+ // <br> in nsMsgCompose::BuildBodyMessageAndSignature().
+ // Don't do the conversion if the user asked explicitly for plain text.
+ data = data.replace(/\r?\n/g, " ");
+ }
+ gBodyFromArgs = true;
+ composeFields.body = data;
+ }
+ }
+ } else if (args.body) {
+ gBodyFromArgs = true;
+ composeFields.body = args.body;
+ }
+ }
+ }
+
+ gComposeType = params.type;
+
+ // Detect correct identity when missing or mismatched. An identity with no
+ // email is likely not valid.
+ // When editing a draft, 'params.identity' is pre-populated with the identity
+ // that created the draft or the identity owning the draft folder for a
+ // "foreign" draft, see ComposeMessage() in mailCommands.js. We don't want the
+ // latter so use the creator identity which could be null.
+ // Only do this detection for drafts and templates.
+ // Redirect will have from set as the original sender and we don't want to
+ // warn about that.
+ if (
+ gComposeType == Ci.nsIMsgCompType.Draft ||
+ gComposeType == Ci.nsIMsgCompType.Template
+ ) {
+ let creatorKey = params.composeFields.creatorIdentityKey;
+ params.identity = creatorKey
+ ? MailServices.accounts.getIdentity(creatorKey)
+ : null;
+ }
+
+ let from = null;
+ // Get the from address from the headers. For Redirect, from is set to
+ // the original author, so don't look at it here.
+ if (params.composeFields.from && gComposeType != Ci.nsIMsgCompType.Redirect) {
+ let fromAddrs = MailServices.headerParser.parseEncodedHeader(
+ params.composeFields.from,
+ null
+ );
+ if (fromAddrs.length) {
+ from = fromAddrs[0].email.toLowerCase();
+ }
+ }
+
+ if (
+ !params.identity ||
+ !params.identity.email ||
+ (from && !emailSimilar(from, params.identity.email))
+ ) {
+ let identities = MailServices.accounts.allIdentities;
+ let suitableCount = 0;
+
+ // Search for a matching identity.
+ if (from) {
+ for (let ident of identities) {
+ if (ident.email && from == ident.email.toLowerCase()) {
+ if (suitableCount == 0) {
+ params.identity = ident;
+ }
+ suitableCount++;
+ if (suitableCount > 1) {
+ // No need to find more, it's already not unique.
+ break;
+ }
+ }
+ }
+ }
+
+ if (!params.identity || !params.identity.email) {
+ let identity = null;
+ // No preset identity and no match, so use the default account.
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ if (defaultAccount) {
+ identity = defaultAccount.defaultIdentity;
+ }
+ if (!identity) {
+ // Get the first identity we have in the list.
+ let identitykey = identityList
+ .getItemAtIndex(0)
+ .getAttribute("identitykey");
+ identity = MailServices.accounts.getIdentity(identitykey);
+ }
+ params.identity = identity;
+ }
+
+ // Warn if no or more than one match was found.
+ // But don't warn for +suffix additions (a+b@c.com).
+ if (
+ from &&
+ (suitableCount > 1 ||
+ (suitableCount == 0 && !emailSimilar(from, params.identity.email)))
+ ) {
+ gComposeNotificationBar.setIdentityWarning(params.identity.identityName);
+ }
+ }
+
+ if (params.identity) {
+ identityList.selectedItem = identityList.getElementsByAttribute(
+ "identitykey",
+ params.identity.key
+ )[0];
+ }
+
+ // Here we set the From from the original message, be it a draft or another
+ // message, for example a template, we want to "edit as new".
+ // Only do this if the message is our own draft or template or any type of reply.
+ if (
+ params.composeFields.from &&
+ (params.composeFields.creatorIdentityKey ||
+ gComposeType == Ci.nsIMsgCompType.Reply ||
+ gComposeType == Ci.nsIMsgCompType.ReplyAll ||
+ gComposeType == Ci.nsIMsgCompType.ReplyToSender ||
+ gComposeType == Ci.nsIMsgCompType.ReplyToGroup ||
+ gComposeType == Ci.nsIMsgCompType.ReplyToSenderAndGroup ||
+ gComposeType == Ci.nsIMsgCompType.ReplyToList)
+ ) {
+ let from = MailServices.headerParser
+ .parseEncodedHeader(params.composeFields.from, null)
+ .join(", ");
+ if (from != identityList.value) {
+ MakeFromFieldEditable(true);
+ identityList.value = from;
+ }
+ }
+ LoadIdentity(true);
+
+ // Get the <editor> element to startup an editor
+ var editorElement = GetCurrentEditorElement();
+
+ // Remember the original message URI. When editing a draft which is a reply
+ // or forwarded message, this gets overwritten by the ancestor's message URI so
+ // the disposition flags ("replied" or "forwarded") can be set on the ancestor.
+ // For our purposes we need the URI of the message being processed, not its
+ // original ancestor.
+ gOriginalMsgURI = params.originalMsgURI;
+ gMsgCompose = MailServices.compose.initCompose(
+ params,
+ window,
+ editorElement.docShell
+ );
+
+ // If a message is a draft, we rely on draft status flags to decide
+ // about encryption setting. Don't set gIsRelatedToEncryptedOriginal
+ // simply because a message was saved as an encrypted draft, because
+ // we save draft messages encrypted as soon as the account is able
+ // to encrypt, regardless of the user's desire for encryption for
+ // this message.
+
+ if (
+ gComposeType != Ci.nsIMsgCompType.Draft &&
+ gComposeType != Ci.nsIMsgCompType.Template &&
+ gEncryptedURIService &&
+ gEncryptedURIService.isEncrypted(gMsgCompose.originalMsgURI)
+ ) {
+ gIsRelatedToEncryptedOriginal = true;
+ }
+
+ gMsgCompose.addMsgSendListener(gSendListener);
+
+ document
+ .getElementById("dsnMenu")
+ .setAttribute("checked", gMsgCompose.compFields.DSN);
+ document
+ .getElementById("cmd_attachVCard")
+ .setAttribute("checked", gMsgCompose.compFields.attachVCard);
+ document
+ .getElementById("cmd_attachPublicKey")
+ .setAttribute("checked", gAttachMyPublicPGPKey);
+ toggleAttachmentReminder(gMsgCompose.compFields.attachmentReminder);
+ initSendFormatMenu();
+
+ let editortype = gMsgCompose.composeHTML ? "htmlmail" : "textmail";
+ editorElement.makeEditable(editortype, true);
+
+ // setEditorType MUST be called before setContentWindow
+ if (gMsgCompose.composeHTML) {
+ initLocalFontFaceMenu(document.getElementById("FontFacePopup"));
+ } else {
+ // We are editing in plain text mode, so hide the formatting menus and the
+ // output format selector.
+ document.getElementById("FormatToolbar").hidden = true;
+ document.getElementById("formatMenu").hidden = true;
+ document.getElementById("insertMenu").hidden = true;
+ document.getElementById("menu_showFormatToolbar").hidden = true;
+ document.getElementById("outputFormatMenu").hidden = true;
+ }
+
+ // Do setup common to Message Composer and Web Composer.
+ EditorSharedStartup();
+ ToggleReturnReceipt(gMsgCompose.compFields.returnReceipt);
+
+ if (params.bodyIsLink) {
+ let body = gMsgCompose.compFields.body;
+ if (gMsgCompose.composeHTML) {
+ let cleanBody;
+ try {
+ cleanBody = decodeURI(body);
+ } catch (e) {
+ cleanBody = body;
+ }
+
+ body = body.replace(/&/g, "&amp;");
+ gMsgCompose.compFields.body =
+ '<br /><a href="' + body + '">' + cleanBody + "</a><br />";
+ } else {
+ gMsgCompose.compFields.body = "\n<" + body + ">\n";
+ }
+ }
+
+ document.getElementById("msgSubject").value = gMsgCompose.compFields.subject;
+
+ // Do not await async calls before registering the stateListener, otherwise it
+ // will miss states.
+ gMsgCompose.RegisterStateListener(stateListener);
+
+ let addedAttachmentItems = await AddAttachments(
+ gMsgCompose.compFields.attachments,
+ false
+ );
+ // If any of the pre-loaded attachments is a cloudFile, this is most probably a
+ // re-opened draft. Restore the cloudFile information.
+ for (let attachmentItem of addedAttachmentItems) {
+ if (
+ attachmentItem.attachment.sendViaCloud &&
+ attachmentItem.attachment.contentLocation &&
+ attachmentItem.attachment.cloudFileAccountKey &&
+ attachmentItem.attachment.cloudPartHeaderData
+ ) {
+ let byteString = atob(attachmentItem.attachment.cloudPartHeaderData);
+ let uploadFromDraft = JSON.parse(
+ MailStringUtils.byteStringToString(byteString)
+ );
+ if (uploadFromDraft && uploadFromDraft.path && uploadFromDraft.name) {
+ let cloudFileUpload;
+ let cloudFileAccount = cloudFileAccounts.getAccount(
+ attachmentItem.attachment.cloudFileAccountKey
+ );
+ let bigFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ bigFile.initWithPath(uploadFromDraft.path);
+
+ if (cloudFileAccount) {
+ // Try to find the upload for the draft attachment in the already known
+ // uploads.
+ cloudFileUpload = cloudFileAccount
+ .getPreviousUploads()
+ .find(
+ upload =>
+ upload.url == attachmentItem.attachment.contentLocation &&
+ upload.url == uploadFromDraft.url &&
+ upload.id == uploadFromDraft.id &&
+ upload.name == uploadFromDraft.name &&
+ upload.size == uploadFromDraft.size &&
+ upload.path == uploadFromDraft.path &&
+ upload.serviceName == uploadFromDraft.serviceName &&
+ upload.serviceIcon == uploadFromDraft.serviceIcon &&
+ upload.serviceUrl == uploadFromDraft.serviceUrl &&
+ upload.downloadPasswordProtected ==
+ uploadFromDraft.downloadPasswordProtected &&
+ upload.downloadLimit == uploadFromDraft.downloadLimit &&
+ upload.downloadExpiryDate == uploadFromDraft.downloadExpiryDate
+ );
+ if (!cloudFileUpload) {
+ // Create a new upload from the data stored in the draft.
+ cloudFileUpload = cloudFileAccount.newUploadForFile(
+ bigFile,
+ uploadFromDraft
+ );
+ }
+ // A restored cloudFile may have been send/used already in a previous
+ // session, or may be changed and reverted again by not saving a draft.
+ // Mark it as immutable.
+ cloudFileAccount.markAsImmutable(cloudFileUpload.id);
+ attachmentItem.cloudFileAccount = cloudFileAccount;
+ attachmentItem.cloudFileUpload = cloudFileUpload;
+ } else {
+ attachmentItem.cloudFileUpload = uploadFromDraft;
+ delete attachmentItem.cloudFileUpload.id;
+ }
+
+ // Restore file information from the linked real file.
+ attachmentItem.attachment.name = uploadFromDraft.name;
+ attachmentItem.attachment.size = uploadFromDraft.size;
+ let bigAttachment;
+ if (bigFile.exists()) {
+ bigAttachment = FileToAttachment(bigFile);
+ }
+ if (bigAttachment && bigAttachment.size == uploadFromDraft.size) {
+ // Remove the temporary html placeholder file.
+ let uri = Services.io
+ .newURI(attachmentItem.attachment.url)
+ .QueryInterface(Ci.nsIFileURL);
+ await IOUtils.remove(uri.file.path);
+
+ attachmentItem.attachment.url = bigAttachment.url;
+ attachmentItem.attachment.contentType = "";
+ attachmentItem.attachment.temporary = false;
+ }
+
+ await updateAttachmentItemProperties(attachmentItem);
+ continue;
+ }
+ }
+ // Did not find the required data in the draft to reconstruct the cloudFile
+ // information. Fall back to no-draft-restore-support.
+ attachmentItem.attachment.sendViaCloud = false;
+ }
+
+ if (Services.prefs.getBoolPref("mail.compose.show_attachment_pane")) {
+ toggleAttachmentPane("show");
+ }
+
+ // Fill custom headers.
+ let otherHeaders = Services.prefs
+ .getCharPref("mail.compose.other.header", "")
+ .split(",")
+ .map(h => h.trim())
+ .filter(Boolean);
+ for (let i = 0; i < otherHeaders.length; i++) {
+ if (gMsgCompose.compFields.otherHeaders[i]) {
+ let row = document.getElementById(`addressRow${otherHeaders[i]}`);
+ addressRowSetVisibility(row, true);
+ let input = document.getElementById(`${otherHeaders[i]}AddrInput`);
+ input.value = gMsgCompose.compFields.otherHeaders[i];
+ }
+ }
+
+ document
+ .getElementById("msgcomposeWindow")
+ .dispatchEvent(
+ new Event("compose-window-init", { bubbles: false, cancelable: true })
+ );
+
+ dispatchAttachmentBucketEvent(
+ "attachments-added",
+ gMsgCompose.compFields.attachments
+ );
+
+ // Add an observer to be called when document is done loading,
+ // which creates the editor.
+ try {
+ GetCurrentCommandManager().addCommandObserver(
+ gMsgEditorCreationObserver,
+ "obs_documentCreated"
+ );
+
+ // Load empty page to create the editor. The "?compose" is there so this
+ // URL does not exactly match "about:blank", which has some drawbacks. In
+ // particular it prevents WebExtension content scripts from running in
+ // this document.
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ editorElement.webNavigation.loadURI(
+ Services.io.newURI("about:blank?compose"),
+ loadURIOptions
+ );
+ } catch (e) {
+ console.error(e);
+ }
+
+ gEditingDraft = gMsgCompose.compFields.draftId;
+
+ // Set up contacts sidebar.
+ let pageURL = document.URL;
+ let contactsSplitter = document.getElementById("contactsSplitter");
+ let contactsShown = Services.xulStore.getValue(
+ pageURL,
+ "contactsSplitter",
+ "shown"
+ );
+ let contactsWidth = Services.xulStore.getValue(
+ pageURL,
+ "contactsSplitter",
+ "width"
+ );
+ contactsSplitter.width =
+ contactsWidth == "" ? null : parseFloat(contactsWidth);
+ setContactsSidebarVisibility(contactsShown == "true", false);
+ contactsSplitter.addEventListener("splitter-resized", () => {
+ let width = contactsSplitter.width;
+ Services.xulStore.setValue(
+ pageURL,
+ "contactsSplitter",
+ "width",
+ width == null ? "" : String(width)
+ );
+ });
+ contactsSplitter.addEventListener("splitter-collapsed", () => {
+ Services.xulStore.setValue(pageURL, "contactsSplitter", "shown", "false");
+ });
+ contactsSplitter.addEventListener("splitter-expanded", () => {
+ Services.xulStore.setValue(pageURL, "contactsSplitter", "shown", "true");
+ });
+
+ // Update the priority button.
+ if (gMsgCompose.compFields.priority) {
+ updatePriorityToolbarButton(gMsgCompose.compFields.priority);
+ }
+
+ gAutoSaveInterval = Services.prefs.getBoolPref("mail.compose.autosave")
+ ? Services.prefs.getIntPref("mail.compose.autosaveinterval") * 60000
+ : 0;
+
+ if (gAutoSaveInterval) {
+ gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval);
+ }
+
+ gAutoSaveKickedIn = false;
+}
+/* eslint-enable complexity */
+
+function splitEmailAddress(aEmail) {
+ let at = aEmail.lastIndexOf("@");
+ return at != -1 ? [aEmail.slice(0, at), aEmail.slice(at + 1)] : [aEmail, ""];
+}
+
+// Emails are equal ignoring +suffixes (email+suffix@example.com).
+function emailSimilar(a, b) {
+ if (!a || !b) {
+ return a == b;
+ }
+ a = splitEmailAddress(a.toLowerCase());
+ b = splitEmailAddress(b.toLowerCase());
+ return a[1] == b[1] && a[0].split("+", 1)[0] == b[0].split("+", 1)[0];
+}
+
+// The new, nice, simple way of getting notified when a new editor has been created
+var gMsgEditorCreationObserver = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "obs_documentCreated") {
+ var editor = GetCurrentEditor();
+ if (editor && GetCurrentCommandManager() == aSubject) {
+ InitEditor();
+ }
+ // Now that we know this document is an editor, update commands now if
+ // the document has focus, or next time it receives focus via
+ // CommandUpdate_MsgCompose()
+ if (gLastWindowToHaveFocus == document.commandDispatcher.focusedWindow) {
+ updateComposeItems();
+ } else {
+ gLastWindowToHaveFocus = null;
+ }
+ }
+ },
+};
+
+/**
+ * Adjust sign/encrypt settings after the identity was switched.
+ *
+ * @param {?nsIMsgIdentity} prevIdentity - The previously selected
+ * identity, when switching to a different identity.
+ * Null on initial identity setup.
+ */
+async function adjustEncryptAfterIdentityChange(prevIdentity) {
+ let identityHasConfiguredSMIME =
+ isSmimeSigningConfigured() || isSmimeEncryptionConfigured();
+
+ let identityHasConfiguredOpenPGP = isPgpConfigured();
+
+ // Show widgets based on the technologies available across all identities.
+ let allEmailIdentities = MailServices.accounts.allIdentities.filter(
+ i => i.email
+ );
+ let anyIdentityHasConfiguredOpenPGP = allEmailIdentities.some(i =>
+ i.getUnicharAttribute("openpgp_key_id")
+ );
+ let anyIdentityHasConfiguredSMIMEEncryption = allEmailIdentities.some(i =>
+ i.getUnicharAttribute("encryption_cert_name")
+ );
+
+ // Disable encryption widgets if this identity has no encryption configured.
+ // However, if encryption is currently enabled, we must keep it enabled,
+ // to allow the user to manually disable encryption (we don't disable
+ // encryption automatically, as the user might have seen that it is
+ // enabled and might rely on it).
+ let e2eeConfigured =
+ identityHasConfiguredOpenPGP || identityHasConfiguredSMIME;
+
+ let autoEnablePref = Services.prefs.getBoolPref(
+ "mail.e2ee.auto_enable",
+ false
+ );
+
+ // If neither OpenPGP nor SMIME are configured for any identity,
+ // then hide the entire menu.
+ let encOpt = document.getElementById("button-encryption-options");
+ if (encOpt) {
+ encOpt.hidden =
+ !anyIdentityHasConfiguredOpenPGP &&
+ !anyIdentityHasConfiguredSMIMEEncryption;
+ encOpt.disabled = !e2eeConfigured && !gSendEncrypted;
+ document.getElementById("encTech_OpenPGP_Toolbar").disabled =
+ !identityHasConfiguredOpenPGP;
+ document.getElementById("encTech_SMIME_Toolbar").disabled =
+ !identityHasConfiguredSMIME;
+ }
+ document.getElementById("encryptionMenu").hidden =
+ !anyIdentityHasConfiguredOpenPGP &&
+ !anyIdentityHasConfiguredSMIMEEncryption;
+
+ // Show menu items only if both technologies are available.
+ document.getElementById("encTech_OpenPGP_Menubar").hidden =
+ !anyIdentityHasConfiguredOpenPGP ||
+ !anyIdentityHasConfiguredSMIMEEncryption;
+ document.getElementById("encTech_SMIME_Menubar").hidden =
+ !anyIdentityHasConfiguredOpenPGP ||
+ !anyIdentityHasConfiguredSMIMEEncryption;
+ document.getElementById("encryptionOptionsSeparator_Menubar").hidden =
+ !anyIdentityHasConfiguredOpenPGP ||
+ !anyIdentityHasConfiguredSMIMEEncryption;
+
+ let encToggle = document.getElementById("button-encryption");
+ if (encToggle) {
+ encToggle.disabled = !e2eeConfigured && !gSendEncrypted;
+ }
+ let sigToggle = document.getElementById("button-signing");
+ if (sigToggle) {
+ sigToggle.disabled = !e2eeConfigured;
+ }
+
+ document.getElementById("encryptionMenu").disabled =
+ !e2eeConfigured && !gSendEncrypted;
+
+ // Enable the encryption menus of the technologies that are configured for
+ // this identity.
+ document.getElementById("encTech_OpenPGP_Menubar").disabled =
+ !identityHasConfiguredOpenPGP;
+
+ document.getElementById("encTech_SMIME_Menubar").disabled =
+ !identityHasConfiguredSMIME;
+
+ if (!prevIdentity) {
+ // For identities without any e2ee setup, we want a good default
+ // technology selection. Avoid a technology that isn't configured
+ // anywhere.
+
+ if (identityHasConfiguredOpenPGP) {
+ gSelectedTechnologyIsPGP = true;
+ } else if (identityHasConfiguredSMIME) {
+ gSelectedTechnologyIsPGP = false;
+ } else {
+ gSelectedTechnologyIsPGP = anyIdentityHasConfiguredOpenPGP;
+ }
+
+ if (identityHasConfiguredOpenPGP) {
+ if (!identityHasConfiguredSMIME) {
+ gSelectedTechnologyIsPGP = true;
+ } else {
+ // both are configured
+ let techPref = gCurrentIdentity.getIntAttribute("e2etechpref");
+ gSelectedTechnologyIsPGP = techPref != 1;
+ }
+ }
+
+ gSendSigned = false;
+
+ if (autoEnablePref) {
+ gSendEncrypted = gIsRelatedToEncryptedOriginal;
+ } else {
+ gSendEncrypted =
+ gIsRelatedToEncryptedOriginal ||
+ ((identityHasConfiguredOpenPGP || identityHasConfiguredSMIME) &&
+ gCurrentIdentity.encryptionPolicy > 0);
+ }
+
+ await checkEncryptionState();
+ return;
+ }
+
+ // Not initialCall (switching from, or changed recipients)
+
+ // If the new identity has only one technology configured,
+ // which is different than the currently selected technology,
+ // then switch over to that other technology.
+ // However, if the new account doesn't have any technology
+ // configured, then it doesn't really matter, so let's keep what's
+ // currently selected for consistency (in case the user switches
+ // the identity again).
+ if (
+ gSelectedTechnologyIsPGP &&
+ !identityHasConfiguredOpenPGP &&
+ identityHasConfiguredSMIME
+ ) {
+ gSelectedTechnologyIsPGP = false;
+ } else if (
+ !gSelectedTechnologyIsPGP &&
+ !identityHasConfiguredSMIME &&
+ identityHasConfiguredOpenPGP
+ ) {
+ gSelectedTechnologyIsPGP = true;
+ }
+
+ if (
+ !autoEnablePref &&
+ !gSendEncrypted &&
+ !gUserTouchedEncryptSubject &&
+ prevIdentity.encryptionPolicy == 0 &&
+ gCurrentIdentity.encryptionPolicy > 0
+ ) {
+ gSendEncrypted = true;
+ }
+
+ await checkEncryptionState();
+}
+
+async function ComposeLoad() {
+ updateTroubleshootMenuItem();
+ let otherHeaders = Services.prefs
+ .getCharPref("mail.compose.other.header", "")
+ .split(",")
+ .map(h => h.trim())
+ .filter(Boolean);
+
+ AddMessageComposeOfflineQuitObserver();
+
+ BondOpenPGP.init();
+
+ // Give the message header a minimum height based on its current height,
+ // before more recipient rows are revealed in #extraAddressRowsArea. This
+ // ensures that the area cannot be shrunk below its current height by the
+ // #headersSplitter.
+ // NOTE: At this stage, we only expect the "To" row to be visible within the
+ // recipients container.
+ let messageHeader = document.getElementById("MsgHeadersToolbar");
+ let recipientsContainer = document.getElementById("recipientsContainer");
+ // In the unlikely situation where the recipients container is already
+ // overflowing, we make sure to increase the minHeight by the overflow.
+ let headerHeight =
+ messageHeader.clientHeight +
+ recipientsContainer.scrollHeight -
+ recipientsContainer.clientHeight;
+ messageHeader.style.minHeight = `${headerHeight}px`;
+
+ // Setup the attachment bucket.
+ gAttachmentBucket = document.getElementById("attachmentBucket");
+
+ let attachmentArea = document.getElementById("attachmentArea");
+ attachmentArea.addEventListener("toggle", attachmentAreaOnToggle);
+
+ // Setup the attachment animation counter.
+ gAttachmentCounter = document.getElementById("newAttachmentIndicator");
+ gAttachmentCounter.addEventListener(
+ "animationend",
+ toggleAttachmentAnimation
+ );
+
+ // Set up the drag & drop event listeners.
+ let messageArea = document.getElementById("messageArea");
+ messageArea.addEventListener("dragover", event =>
+ envelopeDragObserver.onDragOver(event)
+ );
+ messageArea.addEventListener("dragleave", event =>
+ envelopeDragObserver.onDragLeave(event)
+ );
+ messageArea.addEventListener("drop", event =>
+ envelopeDragObserver.onDrop(event)
+ );
+
+ // Setup the attachment overlay animation listeners.
+ let overlay = document.getElementById("dropAttachmentOverlay");
+ overlay.addEventListener("animationend", e => {
+ // Make the overlay constantly visible If the user is dragging a file over
+ // the compose windown.
+ if (e.animationName == "showing-animation") {
+ // We don't remove the "showing" class here since the dragOver event will
+ // keep adding it and we would have a flashing effect.
+ overlay.classList.add("show");
+ return;
+ }
+
+ // Permanently hide the overlay after the hiding animation ended.
+ if (e.animationName == "hiding-animation") {
+ overlay.classList.remove("show", "hiding");
+ // Remove the hover class from the child items to reset the style.
+ document.getElementById("addInline").classList.remove("hover");
+ document.getElementById("addAsAttachment").classList.remove("hover");
+ }
+ });
+
+ if (otherHeaders) {
+ let extraAddressRowsMenu = document.getElementById("extraAddressRowsMenu");
+
+ let existingTypes = Array.from(
+ document.querySelectorAll(".address-row"),
+ row => row.dataset.recipienttype
+ );
+
+ for (let header of otherHeaders) {
+ if (existingTypes.includes(header)) {
+ continue;
+ }
+ existingTypes.push(header);
+
+ header = header.trim();
+ let recipient = {
+ rowId: `addressRow${header}`,
+ labelId: `${header}AddrLabel`,
+ containerId: `${header}AddrContainer`,
+ inputId: `${header}AddrInput`,
+ showRowMenuItemId: `${header}ShowAddressRowMenuItem`,
+ type: header,
+ };
+
+ let newEls = recipientsContainer.buildRecipientRow(recipient, true);
+
+ recipientsContainer.appendChild(newEls.row);
+ extraAddressRowsMenu.appendChild(newEls.showRowMenuItem);
+ }
+ }
+
+ try {
+ SetupCommandUpdateHandlers();
+ await ComposeStartup();
+ } catch (ex) {
+ console.error(ex);
+ Services.prompt.alert(
+ window,
+ getComposeBundle().getString("initErrorDlogTitle"),
+ getComposeBundle().getString("initErrorDlgMessage")
+ );
+
+ MsgComposeCloseWindow();
+ return;
+ }
+
+ ToolbarIconColor.init();
+
+ // initialize the customizeDone method on the customizeable toolbar
+ var toolbox = document.getElementById("compose-toolbox");
+ toolbox.customizeDone = function (aEvent) {
+ MailToolboxCustomizeDone(aEvent, "CustomizeComposeToolbar");
+ };
+
+ updateAttachmentPane();
+ updateAriaLabelsAndTooltipsOfAllAddressRows();
+
+ for (let input of document.querySelectorAll(".address-row-input")) {
+ input.onBeforeHandleKeyDown = event =>
+ addressInputOnBeforeHandleKeyDown(event);
+ }
+
+ top.controllers.appendController(SecurityController);
+ gMsgCompose.compFields.composeSecure = null;
+ gSMFields = Cc[
+ "@mozilla.org/messengercompose/composesecure;1"
+ ].createInstance(Ci.nsIMsgComposeSecure);
+ if (gSMFields) {
+ gMsgCompose.compFields.composeSecure = gSMFields;
+ }
+
+ // Set initial encryption settings.
+ adjustEncryptAfterIdentityChange(null);
+
+ ExtensionParent.apiManager.emit(
+ "extension-browser-inserted",
+ GetCurrentEditorElement()
+ );
+
+ setComposeLabelsAndMenuItems();
+ setKeyboardShortcuts();
+
+ gFocusAreas = [
+ {
+ // #abContactsPanel.
+ // NOTE: If focus is within the browser shadow document, then the
+ // top.document.activeElement points to the browser, which is below
+ // #contactsSidebar.
+ root: document.getElementById("contactsSidebar"),
+ focus: focusContactsSidebarSearchInput,
+ },
+ {
+ // #msgIdentity, .recipient-button and #extraAddressRowsMenuButton.
+ root: document.getElementById("top-gradient-box"),
+ focus: focusMsgIdentity,
+ },
+ ...Array.from(document.querySelectorAll(".address-row"), row => {
+ return { root: row, focus: focusAddressRowInput };
+ }),
+ {
+ root: document.getElementById("subject-box"),
+ focus: focusSubjectInput,
+ },
+ // "#FormatToolbox" cannot receive focus.
+ {
+ // #messageEditor and #FindToolbar
+ root: document.getElementById("messageArea"),
+ focus: focusMsgBody,
+ },
+ {
+ root: document.getElementById("attachmentArea"),
+ focus: focusAttachmentBucket,
+ },
+ {
+ root: document.getElementById("compose-notification-bottom"),
+ focus: focusNotification,
+ },
+ {
+ root: document.getElementById("status-bar"),
+ focus: focusStatusBar,
+ },
+ ];
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+}
+
+/**
+ * Add fluent strings to labels and menu items requiring a shortcut key.
+ */
+function setComposeLabelsAndMenuItems() {
+ // To field.
+ document.l10n.setAttributes(
+ document.getElementById("menu_showToField"),
+ "show-to-row-main-menuitem",
+ {
+ key: SHOW_TO_KEY,
+ }
+ );
+ document.l10n.setAttributes(
+ document.getElementById("addr_toShowAddressRowMenuItem"),
+ "show-to-row-extra-menuitem"
+ );
+ document.l10n.setAttributes(
+ document.getElementById("addr_toShowAddressRowButton"),
+ "show-to-row-button",
+ {
+ key: SHOW_TO_KEY,
+ }
+ );
+
+ // Cc field.
+ document.l10n.setAttributes(
+ document.getElementById("menu_showCcField"),
+ "show-cc-row-main-menuitem",
+ {
+ key: SHOW_CC_KEY,
+ }
+ );
+ document.l10n.setAttributes(
+ document.getElementById("addr_ccShowAddressRowMenuItem"),
+ "show-cc-row-extra-menuitem"
+ );
+ document.l10n.setAttributes(
+ document.getElementById("addr_ccShowAddressRowButton"),
+ "show-cc-row-button",
+ {
+ key: SHOW_CC_KEY,
+ }
+ );
+
+ // Bcc field.
+ document.l10n.setAttributes(
+ document.getElementById("menu_showBccField"),
+ "show-bcc-row-main-menuitem",
+ {
+ key: SHOW_BCC_KEY,
+ }
+ );
+ document.l10n.setAttributes(
+ document.getElementById("addr_bccShowAddressRowMenuItem"),
+ "show-bcc-row-extra-menuitem"
+ );
+ document.l10n.setAttributes(
+ document.getElementById("addr_bccShowAddressRowButton"),
+ "show-bcc-row-button",
+ {
+ key: SHOW_BCC_KEY,
+ }
+ );
+}
+
+/**
+ * Add a keydown document event listener for international keyboard shortcuts.
+ */
+async function setKeyboardShortcuts() {
+ let [filePickerKey, toggleBucketKey] = await l10nCompose.formatValues([
+ { id: "trigger-attachment-picker-key" },
+ { id: "toggle-attachment-pane-key" },
+ ]);
+
+ document.addEventListener("keydown", event => {
+ // Return if we don't have the right modifier combination, CTRL/CMD + SHIFT,
+ // or if the pressed key is a modifier (each modifier will keep firing
+ // keydown event until another key is pressed in addition).
+ if (
+ !(AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) ||
+ !event.shiftKey ||
+ ["Shift", "Control", "Meta"].includes(event.key)
+ ) {
+ return;
+ }
+
+ // Always use lowercase to compare the key and avoid OS inconsistencies:
+ // For Cmd/Ctrl+Shift+A, on Mac, key = "a" vs. on Windows/Linux, key = "A".
+ switch (event.key.toLowerCase()) {
+ // Always prevent the default behavior of the keydown if we intercepted
+ // the key in order to avoid triggering OS specific shortcuts.
+ case filePickerKey.toLowerCase():
+ // Ctrl/Cmd+Shift+A.
+ event.preventDefault();
+ goDoCommand("cmd_attachFile");
+ break;
+ case toggleBucketKey.toLowerCase():
+ // Ctrl/Cmd+Shift+M.
+ event.preventDefault();
+ goDoCommand("cmd_toggleAttachmentPane");
+ break;
+ case SHOW_TO_KEY.toLowerCase():
+ // Ctrl/Cmd+Shift+T.
+ event.preventDefault();
+ showAndFocusAddressRow("addressRowTo");
+ break;
+ case SHOW_CC_KEY.toLowerCase():
+ // Ctrl/Cmd+Shift+C.
+ event.preventDefault();
+ showAndFocusAddressRow("addressRowCc");
+ break;
+ case SHOW_BCC_KEY.toLowerCase():
+ // Ctrl/Cmd+Shift+B.
+ event.preventDefault();
+ showAndFocusAddressRow("addressRowBcc");
+ break;
+ }
+ });
+
+ document.addEventListener("keypress", event => {
+ // If the user presses Esc and the drop attachment overlay is still visible,
+ // call the onDragLeave() method to properly hide it.
+ if (
+ event.key == "Escape" &&
+ document
+ .getElementById("dropAttachmentOverlay")
+ .classList.contains("show")
+ ) {
+ envelopeDragObserver.onDragLeave(event);
+ }
+ });
+}
+
+function ComposeUnload() {
+ // Send notification that the window is going away completely.
+ document
+ .getElementById("msgcomposeWindow")
+ .dispatchEvent(
+ new Event("compose-window-unload", { bubbles: false, cancelable: false })
+ );
+
+ GetCurrentCommandManager().removeCommandObserver(
+ gMsgEditorCreationObserver,
+ "obs_documentCreated"
+ );
+ UnloadCommandUpdateHandlers();
+
+ // In some tests, the window is closed so quickly that the observer
+ // hasn't fired and removed itself yet, so let's remove it here.
+ spellCheckReadyObserver.removeObserver();
+ // Stop spell checker so personal dictionary is saved.
+ enableInlineSpellCheck(false);
+
+ EditorCleanup();
+
+ if (gMsgCompose) {
+ gMsgCompose.removeMsgSendListener(gSendListener);
+ }
+
+ RemoveMessageComposeOfflineQuitObserver();
+ gAttachmentNotifier.shutdown();
+ ToolbarIconColor.uninit();
+
+ // Stop observing dictionary removals.
+ dictionaryRemovalObserver.removeObserver();
+
+ if (gMsgCompose) {
+ // Notify the SendListener that Send has been aborted and Stopped
+ gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT);
+ gMsgCompose.UnregisterStateListener(stateListener);
+ }
+ if (gAutoSaveTimeout) {
+ clearTimeout(gAutoSaveTimeout);
+ }
+ if (msgWindow) {
+ msgWindow.closeWindow();
+ }
+
+ ReleaseGlobalVariables();
+
+ top.controllers.removeController(SecurityController);
+
+ // This destroys the window for us.
+ MsgComposeCloseWindow();
+}
+
+function onEncryptionChoice(value) {
+ switch (value) {
+ case "OpenPGP":
+ if (isPgpConfigured()) {
+ gSelectedTechnologyIsPGP = true;
+ checkEncryptionState();
+ }
+ break;
+
+ case "SMIME":
+ if (isSmimeEncryptionConfigured()) {
+ gSelectedTechnologyIsPGP = false;
+ checkEncryptionState();
+ }
+ break;
+
+ case "enc":
+ toggleEncryptMessage();
+ break;
+
+ case "encsub":
+ gEncryptSubject = !gEncryptSubject;
+ gUserTouchedEncryptSubject = true;
+ updateEncryptedSubject();
+ break;
+
+ case "sig":
+ toggleGlobalSignMessage();
+ break;
+
+ case "status":
+ showMessageComposeSecurityStatus();
+ break;
+
+ case "manager":
+ openKeyManager();
+ break;
+ }
+}
+
+var SecurityController = {
+ supportsCommand(command) {
+ switch (command) {
+ case "cmd_viewSecurityStatus":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled(command) {
+ switch (command) {
+ case "cmd_viewSecurityStatus":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+};
+
+function updateEncryptOptionsMenuElements() {
+ let encOpt = document.getElementById("button-encryption-options");
+ if (encOpt) {
+ document.l10n.setAttributes(
+ encOpt,
+ gSelectedTechnologyIsPGP
+ ? "encryption-options-openpgp"
+ : "encryption-options-smime"
+ );
+ document.l10n.setAttributes(
+ document.getElementById("menu_recipientStatus_Toolbar"),
+ gSelectedTechnologyIsPGP ? "menu-manage-keys" : "menu-view-certificates"
+ );
+ document.getElementById("menu_securityEncryptSubject_Toolbar").hidden =
+ !gSelectedTechnologyIsPGP;
+ }
+ document.l10n.setAttributes(
+ document.getElementById("menu_recipientStatus_Menubar"),
+ gSelectedTechnologyIsPGP ? "menu-manage-keys" : "menu-view-certificates"
+ );
+ document.getElementById("menu_securityEncryptSubject_Menubar").hidden =
+ !gSelectedTechnologyIsPGP;
+}
+
+/**
+ * Update the aria labels of all non-custom address inputs and all pills in the
+ * addressing area. Also update the tooltips of the close labels of all address
+ * rows, including custom header fields.
+ */
+async function updateAriaLabelsAndTooltipsOfAllAddressRows() {
+ for (let row of document
+ .getElementById("recipientsContainer")
+ .querySelectorAll(".address-row")) {
+ updateAriaLabelsOfAddressRow(row);
+ updateTooltipsOfAddressRow(row);
+ }
+}
+
+/**
+ * Update the aria labels of the address input and all pills of an address row.
+ * This is needed whenever a pill gets added or removed, because the aria label
+ * of each pill contains the current count of all pills in that row ("1 of n").
+ *
+ * @param {Element} row - The address row.
+ */
+async function updateAriaLabelsOfAddressRow(row) {
+ // Bail out for custom header input where pills are disabled.
+ if (row.classList.contains("address-row-raw")) {
+ return;
+ }
+ let input = row.querySelector(".address-row-input");
+
+ let type = row.querySelector(".address-label-container > label").value;
+ let pills = row.querySelectorAll("mail-address-pill");
+
+ input.setAttribute(
+ "aria-label",
+ await l10nCompose.formatValue("address-input-type-aria-label", {
+ type,
+ count: pills.length,
+ })
+ );
+
+ for (let pill of pills) {
+ pill.setAttribute(
+ "aria-label",
+ await l10nCompose.formatValue("pill-aria-label", {
+ email: pill.fullAddress,
+ count: pills.length,
+ })
+ );
+ }
+}
+
+/**
+ * Update the tooltip of the close label of an address row.
+ *
+ * @param {Element} row - The address row.
+ */
+function updateTooltipsOfAddressRow(row) {
+ let type = row.querySelector(".address-label-container > label").value;
+ let el = row.querySelector(".remove-field-button");
+ document.l10n.setAttributes(el, "remove-address-row-button", { type });
+}
+
+function onSendSMIME() {
+ let emailAddresses = [];
+
+ try {
+ if (!gMsgCompose.compFields.composeSecure.requireEncryptMessage) {
+ return;
+ }
+
+ for (let email of getEncryptionCompatibleRecipients()) {
+ if (!gSMFields.haveValidCertForEmail(email)) {
+ emailAddresses.push(email);
+ }
+ }
+ } catch (e) {
+ return;
+ }
+
+ if (emailAddresses.length == 0) {
+ return;
+ }
+
+ // The rules here: If the current identity has a directoryServer set, then
+ // use that, otherwise, try the global preference instead.
+
+ let autocompleteDirectory;
+
+ // Does the current identity override the global preference?
+ if (gCurrentIdentity.overrideGlobalPref) {
+ autocompleteDirectory = gCurrentIdentity.directoryServer;
+ } else if (Services.prefs.getBoolPref("ldap_2.autoComplete.useDirectory")) {
+ // Try the global one
+ autocompleteDirectory = Services.prefs.getCharPref(
+ "ldap_2.autoComplete.directoryServer"
+ );
+ }
+
+ if (autocompleteDirectory) {
+ window.openDialog(
+ "chrome://messenger-smime/content/certFetchingStatus.xhtml",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ autocompleteDirectory,
+ emailAddresses
+ );
+ }
+}
+
+// Add-ons can override this to customize the behavior.
+function DoSpellCheckBeforeSend() {
+ return Services.prefs.getBoolPref("mail.SpellCheckBeforeSend");
+}
+
+/**
+ * Updates gMsgCompose.compFields to match the UI.
+ *
+ * @returns {nsIMsgCompFields}
+ */
+function GetComposeDetails() {
+ let msgCompFields = gMsgCompose.compFields;
+
+ Recipients2CompFields(msgCompFields);
+ let addresses = MailServices.headerParser.makeFromDisplayAddress(
+ document.getElementById("msgIdentity").value
+ );
+ msgCompFields.from = MailServices.headerParser.makeMimeHeader(addresses);
+ msgCompFields.subject = document.getElementById("msgSubject").value;
+ Attachments2CompFields(msgCompFields);
+
+ return msgCompFields;
+}
+
+/**
+ * Updates the UI to match newValues.
+ *
+ * @param {object} newValues - New values to use. Values that should not change
+ * should be null or not present.
+ * @param {string} [newValues.to]
+ * @param {string} [newValues.cc]
+ * @param {string} [newValues.bcc]
+ * @param {string} [newValues.replyTo]
+ * @param {string} [newValues.newsgroups]
+ * @param {string} [newValues.followupTo]
+ * @param {string} [newValues.subject]
+ * @param {string} [newValues.body]
+ * @param {string} [newValues.plainTextBody]
+ */
+function SetComposeDetails(newValues) {
+ if (newValues.identityKey !== null) {
+ let identityList = document.getElementById("msgIdentity");
+ for (let menuItem of identityList.menupopup.children) {
+ if (menuItem.getAttribute("identitykey") == newValues.identityKey) {
+ identityList.selectedItem = menuItem;
+ LoadIdentity(false);
+ break;
+ }
+ }
+ }
+ CompFields2Recipients(newValues);
+ if (typeof newValues.subject == "string") {
+ gMsgCompose.compFields.subject = document.getElementById(
+ "msgSubject"
+ ).value = newValues.subject;
+ SetComposeWindowTitle();
+ }
+ if (
+ typeof newValues.body == "string" &&
+ typeof newValues.plainTextBody == "string"
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ let editor = GetCurrentEditor();
+ if (typeof newValues.body == "string") {
+ if (!IsHTMLEditor()) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ editor.rebuildDocumentFromSource(newValues.body);
+ gMsgCompose.bodyModified = true;
+ }
+ if (typeof newValues.plainTextBody == "string") {
+ editor.selectAll();
+ // Remove \r from line endings, which cause extra newlines (bug 1672407).
+ let mailEditor = editor.QueryInterface(Ci.nsIEditorMailSupport);
+ if (newValues.plainTextBody === "") {
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+ } else {
+ mailEditor.insertTextWithQuotations(
+ newValues.plainTextBody.replaceAll("\r\n", "\n")
+ );
+ }
+ gMsgCompose.bodyModified = true;
+ }
+ gContentChanged = true;
+}
+
+/**
+ * Handles message sending operations.
+ *
+ * @param {nsIMsgCompDeliverMode} mode - The delivery mode of the operation.
+ */
+async function GenericSendMessage(msgType) {
+ let msgCompFields = GetComposeDetails();
+
+ // Some other msgCompFields have already been updated instantly in their
+ // respective toggle functions, e.g. ToggleReturnReceipt(), ToggleDSN(),
+ // ToggleAttachVCard(), and toggleAttachmentReminder().
+
+ let sending =
+ msgType == Ci.nsIMsgCompDeliverMode.Now ||
+ msgType == Ci.nsIMsgCompDeliverMode.Later ||
+ msgType == Ci.nsIMsgCompDeliverMode.Background;
+
+ // Notify about a new message being prepared for sending.
+ window.dispatchEvent(
+ new CustomEvent("compose-prepare-message-start", {
+ detail: { msgType },
+ })
+ );
+
+ try {
+ if (sending) {
+ // Since the onBeforeSend event can manipulate compose details, execute it
+ // before the final sanity checks.
+ try {
+ await new Promise((resolve, reject) => {
+ let beforeSendEvent = new CustomEvent("beforesend", {
+ cancelable: true,
+ detail: {
+ resolve,
+ reject,
+ },
+ });
+ window.dispatchEvent(beforeSendEvent);
+ if (!beforeSendEvent.defaultPrevented) {
+ resolve();
+ }
+ });
+ } catch (ex) {
+ throw new Error(`Send aborted by an onBeforeSend event`);
+ }
+
+ expandRecipients();
+ // Check if e-mail addresses are complete, in case user turned off
+ // autocomplete to local domain.
+ if (!CheckValidEmailAddress(msgCompFields)) {
+ throw new Error(`Send aborted: invalid recipient address found`);
+ }
+
+ // Do we need to check the spelling?
+ if (DoSpellCheckBeforeSend()) {
+ // We disable spellcheck for the following -subject line, attachment
+ // pane, identity and addressing widget therefore we need to explicitly
+ // focus on the mail body when we have to do a spellcheck.
+ focusMsgBody();
+ window.cancelSendMessage = false;
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdSpellCheck.xhtml",
+ "_blank",
+ "dialog,close,titlebar,modal,resizable",
+ true,
+ true,
+ false
+ );
+
+ if (window.cancelSendMessage) {
+ throw new Error(`Send aborted by the user: spelling errors found`);
+ }
+ }
+
+ // Strip trailing spaces and long consecutive WSP sequences from the
+ // subject line to prevent getting only WSP chars on a folded line.
+ let subject = msgCompFields.subject;
+ let fixedSubject = subject.replace(/\s{74,}/g, " ").trimRight();
+ if (fixedSubject != subject) {
+ subject = fixedSubject;
+ msgCompFields.subject = fixedSubject;
+ document.getElementById("msgSubject").value = fixedSubject;
+ }
+
+ // Remind the person if there isn't a subject
+ if (subject == "") {
+ if (
+ Services.prompt.confirmEx(
+ window,
+ getComposeBundle().getString("subjectEmptyTitle"),
+ getComposeBundle().getString("subjectEmptyMessage"),
+ Services.prompt.BUTTON_TITLE_IS_STRING *
+ Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING *
+ Services.prompt.BUTTON_POS_1,
+ getComposeBundle().getString("sendWithEmptySubjectButton"),
+ getComposeBundle().getString("cancelSendingButton"),
+ null,
+ null,
+ { value: 0 }
+ ) == 1
+ ) {
+ document.getElementById("msgSubject").focus();
+ throw new Error(`Send aborted by the user: subject missing`);
+ }
+ }
+
+ // Attachment Reminder: Alert the user if
+ // - the user requested "Remind me later" from either the notification bar or the menu
+ // (alert regardless of the number of files already attached: we can't guess for how many
+ // or which files users want the reminder, and guessing wrong will annoy them a lot), OR
+ // - the aggressive pref is set and the latest notification is still showing (implying
+ // that the message has no attachment(s) yet, message still contains some attachment
+ // keywords, and notification was not dismissed).
+ if (
+ gManualAttachmentReminder ||
+ (Services.prefs.getBoolPref(
+ "mail.compose.attachment_reminder_aggressive"
+ ) &&
+ gComposeNotification.getNotificationWithValue("attachmentReminder"))
+ ) {
+ let flags =
+ Services.prompt.BUTTON_POS_0 *
+ Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING;
+ let hadForgotten = Services.prompt.confirmEx(
+ window,
+ getComposeBundle().getString("attachmentReminderTitle"),
+ getComposeBundle().getString("attachmentReminderMsg"),
+ flags,
+ getComposeBundle().getString("attachmentReminderFalseAlarm"),
+ getComposeBundle().getString("attachmentReminderYesIForgot"),
+ null,
+ null,
+ { value: 0 }
+ );
+ // Deactivate manual attachment reminder after showing the alert to avoid alert loop.
+ // We also deactivate reminder when user ignores alert with [x] or [ESC].
+ if (gManualAttachmentReminder) {
+ toggleAttachmentReminder(false);
+ }
+
+ if (hadForgotten) {
+ throw new Error(`Send aborted by the user: attachment missing`);
+ }
+ }
+
+ // Aggressive many public recipients prompt.
+ let publicRecipientCount = getPublicAddressPillsCount();
+ if (
+ Services.prefs.getBoolPref(
+ "mail.compose.warn_public_recipients.aggressive"
+ ) &&
+ publicRecipientCount >=
+ Services.prefs.getIntPref(
+ "mail.compose.warn_public_recipients.threshold"
+ )
+ ) {
+ let flags =
+ Services.prompt.BUTTON_POS_0 *
+ Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING;
+ let [title, msg, cancel, send] = l10nComposeSync.formatValuesSync([
+ "many-public-recipients-prompt-title",
+ {
+ id: "many-public-recipients-prompt-msg",
+ args: { count: getPublicAddressPillsCount() },
+ },
+ "many-public-recipients-prompt-cancel",
+ "many-public-recipients-prompt-send",
+ ]);
+ let willCancel = Services.prompt.confirmEx(
+ window,
+ title,
+ msg,
+ flags,
+ send,
+ cancel,
+ null,
+ null,
+ { value: 0 }
+ );
+
+ if (willCancel) {
+ if (!gRecipientObserver) {
+ // Re-create this observer as it is destroyed when the user dismisses
+ // the warning.
+ gRecipientObserver = new MutationObserver(function (mutations) {
+ if (mutations.some(m => m.type == "childList")) {
+ checkPublicRecipientsLimit();
+ }
+ });
+ }
+ checkPublicRecipientsLimit();
+ throw new Error(
+ `Send aborted by the user: too many public recipients found`
+ );
+ }
+ }
+
+ // Check if the user tries to send a message to a newsgroup through a mail
+ // account.
+ var currentAccountKey = getCurrentAccountKey();
+ let account = MailServices.accounts.getAccount(currentAccountKey);
+ if (
+ account.incomingServer.type != "nntp" &&
+ msgCompFields.newsgroups != ""
+ ) {
+ const kDontAskAgainPref = "mail.compose.dontWarnMail2Newsgroup";
+ // default to ask user if the pref is not set
+ let dontAskAgain = Services.prefs.getBoolPref(kDontAskAgainPref);
+ if (!dontAskAgain) {
+ let checkbox = { value: false };
+ let okToProceed = Services.prompt.confirmCheck(
+ window,
+ getComposeBundle().getString("noNewsgroupSupportTitle"),
+ getComposeBundle().getString("recipientDlogMessage"),
+ getComposeBundle().getString("CheckMsg"),
+ checkbox
+ );
+ if (!okToProceed) {
+ throw new Error(`Send aborted by the user: wrong account used`);
+ }
+
+ if (checkbox.value) {
+ Services.prefs.setBoolPref(kDontAskAgainPref, true);
+ }
+ }
+
+ // remove newsgroups to prevent news_p to be set
+ // in nsMsgComposeAndSend::DeliverMessage()
+ msgCompFields.newsgroups = "";
+ }
+
+ if (Services.prefs.getBoolPref("mail.compose.add_link_preview", true)) {
+ // Remove any card "close" button from content before sending.
+ for (let close of getBrowser().contentDocument.querySelectorAll(
+ ".moz-card .remove-card"
+ )) {
+ close.remove();
+ }
+ }
+
+ let sendFormat = determineSendFormat();
+ switch (sendFormat) {
+ case Ci.nsIMsgCompSendFormat.PlainText:
+ msgCompFields.forcePlainText = true;
+ msgCompFields.useMultipartAlternative = false;
+ break;
+ case Ci.nsIMsgCompSendFormat.HTML:
+ msgCompFields.forcePlainText = false;
+ msgCompFields.useMultipartAlternative = false;
+ break;
+ case Ci.nsIMsgCompSendFormat.Both:
+ msgCompFields.forcePlainText = false;
+ msgCompFields.useMultipartAlternative = true;
+ break;
+ default:
+ throw new Error(`Invalid send format ${sendFormat}`);
+ }
+ }
+
+ await CompleteGenericSendMessage(msgType);
+ window.dispatchEvent(new CustomEvent("compose-prepare-message-success"));
+ } catch (exception) {
+ console.error(exception);
+ window.dispatchEvent(
+ new CustomEvent("compose-prepare-message-failure", {
+ detail: { exception },
+ })
+ );
+ }
+}
+
+/**
+ * Finishes message sending. This should ONLY be called directly from
+ * GenericSendMessage. This is a separate function so that it can be easily mocked
+ * in tests.
+ *
+ * @param msgType nsIMsgCompDeliverMode of the operation.
+ */
+async function CompleteGenericSendMessage(msgType) {
+ // hook for extra compose pre-processing
+ Services.obs.notifyObservers(window, "mail:composeOnSend");
+
+ if (!gSelectedTechnologyIsPGP) {
+ gMsgCompose.compFields.composeSecure.requireEncryptMessage = gSendEncrypted;
+ gMsgCompose.compFields.composeSecure.signMessage = gSendSigned;
+ onSendSMIME();
+ }
+
+ let sendError = null;
+ try {
+ // Just before we try to send the message, fire off the
+ // compose-send-message event for listeners, so they can do
+ // any pre-security work before sending.
+ var event = document.createEvent("UIEvents");
+ event.initEvent("compose-send-message", false, true);
+ var msgcomposeWindow = document.getElementById("msgcomposeWindow");
+ msgcomposeWindow.setAttribute("msgtype", msgType);
+ msgcomposeWindow.dispatchEvent(event);
+ if (event.defaultPrevented) {
+ throw Components.Exception(
+ "compose-send-message prevented",
+ Cr.NS_ERROR_ABORT
+ );
+ }
+
+ gAutoSaving = msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft;
+
+ // disable the ui if we're not auto-saving
+ if (!gAutoSaving) {
+ ToggleWindowLock(true);
+ } else {
+ // If we're auto saving, mark the body as not changed here, and not
+ // when the save is done, because the user might change it between now
+ // and when the save is done.
+ SetContentAndBodyAsUnmodified();
+ }
+
+ // Keep track of send/saved cloudFiles and mark them as immutable.
+ let items = [...gAttachmentBucket.itemChildren];
+ for (let item of items) {
+ if (item.attachment.sendViaCloud && item.cloudFileAccount) {
+ item.cloudFileAccount.markAsImmutable(item.cloudFileUpload.id);
+ }
+ }
+
+ var progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ if (progress) {
+ progress.registerListener(progressListener);
+ if (
+ msgType == Ci.nsIMsgCompDeliverMode.Save ||
+ msgType == Ci.nsIMsgCompDeliverMode.SaveAsDraft ||
+ msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft ||
+ msgType == Ci.nsIMsgCompDeliverMode.SaveAsTemplate
+ ) {
+ gSaveOperationInProgress = true;
+ } else {
+ gSendOperationInProgress = true;
+ }
+ }
+ msgWindow.domWindow = window;
+ msgWindow.rootDocShell.allowAuth = true;
+ await gMsgCompose.sendMsg(
+ msgType,
+ gCurrentIdentity,
+ getCurrentAccountKey(),
+ msgWindow,
+ progress
+ );
+ } catch (ex) {
+ console.error("GenericSendMessage FAILED: " + ex);
+ ToggleWindowLock(false);
+ sendError = ex;
+ }
+
+ if (
+ msgType == Ci.nsIMsgCompDeliverMode.Now ||
+ msgType == Ci.nsIMsgCompDeliverMode.Later ||
+ msgType == Ci.nsIMsgCompDeliverMode.Background
+ ) {
+ window.dispatchEvent(new CustomEvent("aftersend"));
+
+ let maxSize =
+ Services.prefs.getIntPref("mail.compose.big_attachments.threshold_kb") *
+ 1024;
+ let items = [...gAttachmentBucket.itemChildren];
+
+ // When any big attachment is not sent via filelink, increment
+ // `tb.filelink.ignored`.
+ if (
+ items.some(
+ item => item.attachment.size >= maxSize && !item.attachment.sendViaCloud
+ )
+ ) {
+ Services.telemetry.scalarAdd("tb.filelink.ignored", 1);
+ }
+ } else if (
+ msgType == Ci.nsIMsgCompDeliverMode.Save ||
+ msgType == Ci.nsIMsgCompDeliverMode.SaveAsDraft ||
+ msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft ||
+ msgType == Ci.nsIMsgCompDeliverMode.SaveAsTemplate
+ ) {
+ window.dispatchEvent(new CustomEvent("aftersave"));
+ }
+
+ if (sendError) {
+ throw sendError;
+ }
+}
+
+/**
+ * Check if the given email address is valid (contains an @).
+ *
+ * @param {string} address - The email address string to check.
+ */
+function isValidAddress(address) {
+ return address.includes("@", 1) && !address.endsWith("@");
+}
+
+/**
+ * Check if the given news address is valid (contains a dot).
+ *
+ * @param {string} address - The news address string to check.
+ */
+function isValidNewsAddress(address) {
+ return address.includes(".", 1) && !address.endsWith(".");
+}
+
+/**
+ * Force the focus on the autocomplete input if the user clicks on an empty
+ * area of the address container.
+ *
+ * @param {Event} event - the event triggered by the click.
+ */
+function focusAddressInputOnClick(event) {
+ let container = event.target;
+ if (container.classList.contains("address-container")) {
+ container.querySelector(".address-row-input").focus();
+ }
+}
+
+/**
+ * Keep the Send buttons disabled until any recipient is entered.
+ */
+function updateSendLock() {
+ gSendLocked = true;
+ if (!gMsgCompose) {
+ return;
+ }
+
+ const addressRows = [
+ "toAddrContainer",
+ "ccAddrContainer",
+ "bccAddrContainer",
+ "newsgroupsAddrContainer",
+ ];
+
+ for (let parentID of addressRows) {
+ if (!gSendLocked) {
+ break;
+ }
+
+ let parent = document.getElementById(parentID);
+
+ if (!parent) {
+ continue;
+ }
+
+ for (let address of parent.querySelectorAll(".address-pill")) {
+ let listNames = MimeParser.parseHeaderField(
+ address.fullAddress,
+ MimeParser.HEADER_ADDRESS
+ );
+ let isMailingList =
+ listNames.length > 0 &&
+ MailServices.ab.mailListNameExists(listNames[0].name);
+
+ if (
+ isValidAddress(address.emailAddress) ||
+ isMailingList ||
+ address.emailInput.classList.contains("news-input")
+ ) {
+ gSendLocked = false;
+ break;
+ }
+ }
+ }
+
+ // Check the non pillified input text inside the autocomplete input fields.
+ for (let input of document.querySelectorAll(
+ ".address-row:not(.hidden):not(.address-row-raw) .address-row-input"
+ )) {
+ let inputValueTrim = input.value.trim();
+ // If there's no text in the input, proceed with next input.
+ if (!inputValueTrim) {
+ continue;
+ }
+ // If text contains " >> " (typically from an unfinished autocompletion),
+ // lock Send and return.
+ if (inputValueTrim.includes(" >> ")) {
+ gSendLocked = true;
+ return;
+ }
+
+ // If we find at least one valid pill, and in spite of potential other
+ // invalid pills or invalid addresses in the input, enable the Send button.
+ // It might be disabled again if the above autocomplete artifact is present
+ // in a subsequent row, to prevent sending the artifact as a valid address.
+ if (
+ input.classList.contains("news-input")
+ ? isValidNewsAddress(inputValueTrim)
+ : isValidAddress(inputValueTrim)
+ ) {
+ gSendLocked = false;
+ }
+ }
+}
+
+/**
+ * Check if the entered addresses are valid and alert the user if they are not.
+ *
+ * @param aMsgCompFields A nsIMsgCompFields object containing the fields to check.
+ */
+function CheckValidEmailAddress(aMsgCompFields) {
+ let invalidStr;
+ let recipientCount = 0;
+ // Check that each of the To, CC, and BCC recipients contains a '@'.
+ for (let type of ["to", "cc", "bcc"]) {
+ let recipients = aMsgCompFields.splitRecipients(
+ aMsgCompFields[type],
+ false
+ );
+ // MsgCompFields contains only non-empty recipients.
+ recipientCount += recipients.length;
+ for (let recipient of recipients) {
+ if (!isValidAddress(recipient)) {
+ invalidStr = recipient;
+ break;
+ }
+ }
+ if (invalidStr) {
+ break;
+ }
+ }
+
+ if (recipientCount == 0 && aMsgCompFields.newsgroups.trim() == "") {
+ Services.prompt.alert(
+ window,
+ getComposeBundle().getString("addressInvalidTitle"),
+ getComposeBundle().getString("noRecipients")
+ );
+ return false;
+ }
+
+ if (invalidStr) {
+ Services.prompt.alert(
+ window,
+ getComposeBundle().getString("addressInvalidTitle"),
+ getComposeBundle().getFormattedString("addressInvalid", [invalidStr], 1)
+ );
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Cycle through all the currently visible autocomplete addressing rows and
+ * generate pills for those inputs with leftover strings. Do the same if we
+ * have a pill currently being edited. This is necessary in case a user writes
+ * an extra address and clicks "Send" or "Save as..." before the text is
+ * converted into a pill. The input onBlur doesn't work if the click interaction
+ * happens on the window's menu bar.
+ */
+async function pillifyRecipients() {
+ for (let input of document.querySelectorAll(
+ ".address-row:not(.hidden):not(.address-row-raw) .address-row-input"
+ )) {
+ // If we find a leftover string in the input field, create a pill. If the
+ // newly created pill is not a valid address, the sending will stop.
+ if (input.value.trim()) {
+ recipientAddPills(input);
+ }
+ }
+
+ // Update the currently editing pill, if any.
+ // It's impossible to edit more than one pill at once.
+ await document.querySelector("mail-address-pill.editing")?.updatePill();
+}
+
+/**
+ * Handle the dragover event on a recipient disclosure label.
+ *
+ * @param {Event} - The DOM dragover event on a recipient disclosure label.
+ */
+function showAddressRowButtonOnDragover(event) {
+ // Prevent dragover event's default action (which resets the current drag
+ // operation to "none").
+ event.preventDefault();
+}
+
+/**
+ * Handle the drop event on a recipient disclosure label.
+ *
+ * @param {Event} - The DOM drop event on a recipient disclosure label.
+ */
+function showAddressRowButtonOnDrop(event) {
+ if (event.dataTransfer.types.includes("text/pills")) {
+ // If the dragged data includes the type "text/pills", we believe that
+ // the user is dragging our own pills, so we try to move the selected pills
+ // to the address row of the recipient label they were dropped on (Cc, Bcc,
+ // etc.), which will also show the row if needed. If there are no selected
+ // pills (so "text/pills" was generated elsewhere), moveSelectedPills() will
+ // bail out and we'll do nothing.
+ let row = document.getElementById(event.target.dataset.addressRow);
+ document.getElementById("recipientsContainer").moveSelectedPills(row);
+ }
+}
+
+/**
+ * Command handler: Cut the selected pills.
+ */
+function cutSelectedPillsOnCommand() {
+ document.getElementById("recipientsContainer").cutSelectedPills();
+}
+
+/**
+ * Command handler: Copy the selected pills.
+ */
+function copySelectedPillsOnCommand() {
+ document.getElementById("recipientsContainer").copySelectedPills();
+}
+
+/**
+ * Command handler: Select the focused pill and all siblings in the same
+ * address row.
+ *
+ * @param {Element} focusPill - The focused <mail-address-pill> element.
+ */
+function selectAllSiblingPillsOnCommand(focusPill) {
+ let recipientsContainer = document.getElementById("recipientsContainer");
+ // First deselect all pills to ensure that no pills outside the current
+ // address row are selected, e.g. when this action was triggered from
+ // context menu on already selected pill(s).
+ recipientsContainer.deselectAllPills();
+ // Select all pills of the current address row.
+ recipientsContainer.selectSiblingPills(focusPill);
+}
+
+/**
+ * Command handler: Select all recipient pills in the addressing area.
+ */
+function selectAllPillsOnCommand() {
+ document.getElementById("recipientsContainer").selectAllPills();
+}
+
+/**
+ * Command handler: Delete the selected pills.
+ */
+function deleteSelectedPillsOnCommand() {
+ document.getElementById("recipientsContainer").removeSelectedPills();
+}
+
+/**
+ * Command handler: Move the selected pills to another address row.
+ *
+ * @param {string} rowId - The id of the address row to move to.
+ */
+function moveSelectedPillsOnCommand(rowId) {
+ document
+ .getElementById("recipientsContainer")
+ .moveSelectedPills(document.getElementById(rowId));
+}
+
+/**
+ * Check if there are too many public recipients and offer to send them as BCC.
+ */
+function checkPublicRecipientsLimit() {
+ let notification = gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+
+ let recipLimit = Services.prefs.getIntPref(
+ "mail.compose.warn_public_recipients.threshold"
+ );
+
+ let publicAddressPillsCount = getPublicAddressPillsCount();
+
+ if (publicAddressPillsCount < recipLimit) {
+ if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+ return;
+ }
+
+ // Reuse the existing notification since one is shown already.
+ if (notification) {
+ if (publicAddressPillsCount > 1) {
+ document.l10n.setAttributes(
+ notification.messageText,
+ "public-recipients-notice-multi",
+ {
+ count: publicAddressPillsCount,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ notification.messageText,
+ "public-recipients-notice-single"
+ );
+ }
+ return;
+ }
+
+ // Construct the notification as we don't have one.
+ let bccButton = {
+ "l10n-id": "many-public-recipients-bcc",
+ callback() {
+ // Get public addresses before we remove the pills.
+ let publicAddresses = getPublicAddressPills().map(
+ pill => pill.fullAddress
+ );
+
+ addressRowClearPills(document.getElementById("addressRowTo"));
+ addressRowClearPills(document.getElementById("addressRowCc"));
+ // Add previously public address pills to Bcc address row and select them.
+ let bccRow = document.getElementById("addressRowBcc");
+ addressRowAddRecipientsArray(bccRow, publicAddresses, true);
+ // Focus last added pill to prevent sticky selection with focus elsewhere.
+ bccRow.querySelector("mail-address-pill:last-of-type").focus();
+ return false;
+ },
+ };
+
+ let ignoreButton = {
+ "l10n-id": "many-public-recipients-ignore",
+ callback() {
+ gRecipientObserver.disconnect();
+ gRecipientObserver = null;
+ // After closing notification with `Keep Recipients Public`, actively
+ // manage focus to prevent weird focus change e.g. to Contacts Sidebar.
+ // If focus was in addressing area before, restore that as the user might
+ // dismiss the notification when it appears while still adding recipients.
+ if (gLastFocusElement?.classList.contains("address-input")) {
+ gLastFocusElement.focus();
+ return false;
+ }
+
+ // Otherwise if there's no subject yet, focus that (ux-error-prevention).
+ let msgSubject = document.getElementById("msgSubject");
+ if (!msgSubject.value) {
+ msgSubject.focus();
+ return false;
+ }
+
+ // Otherwise default to focusing message body.
+ document.getElementById("messageEditor").focus();
+ return false;
+ },
+ };
+
+ // NOTE: setting "public-recipients-notice-single" below, after the notification
+ // has been appended, so that the notification can be found and no further
+ // notifications are appended.
+ notification = gComposeNotification.appendNotification(
+ "warnPublicRecipientsNotification",
+ {
+ label: "", // "public-recipients-notice-single"
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ eventCallback(state) {
+ if (state == "dismissed") {
+ ignoreButton.callback();
+ }
+ },
+ },
+ [bccButton, ignoreButton]
+ );
+
+ if (notification) {
+ if (publicAddressPillsCount > 1) {
+ document.l10n.setAttributes(
+ notification.messageText,
+ "public-recipients-notice-multi",
+ {
+ count: publicAddressPillsCount,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ notification.messageText,
+ "public-recipients-notice-single"
+ );
+ }
+ }
+}
+
+/**
+ * Get all the address pills in the "To" and "Cc" fields.
+ *
+ * @returns {Element[]} All <mail-address-pill> elements in "To" and "CC" fields.
+ */
+function getPublicAddressPills() {
+ return [
+ ...document.querySelectorAll("#toAddrContainer > mail-address-pill"),
+ ...document.querySelectorAll("#ccAddrContainer > mail-address-pill"),
+ ];
+}
+
+/**
+ * Gets the count of all the address pills in the "To" and "Cc" fields. This
+ * takes mailing lists into consideration as well.
+ */
+function getPublicAddressPillsCount() {
+ let pills = getPublicAddressPills();
+ return pills.reduce(
+ (total, pill) =>
+ pill.isMailList ? total + pill.listAddressCount : total + 1,
+ 0
+ );
+}
+
+/**
+ * Check for Bcc recipients in an encrypted message and warn the user.
+ * The warning is not shown if the only Bcc recipient is the sender.
+ */
+async function checkEncryptedBccRecipients() {
+ let notification = gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ );
+
+ if (!gWantCannotEncryptBCCNotification) {
+ if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+ return;
+ }
+
+ let bccRecipients = [
+ ...document.querySelectorAll("#bccAddrContainer > mail-address-pill"),
+ ];
+ let bccIsSender = bccRecipients.every(
+ pill => pill.emailAddress == gCurrentIdentity.email
+ );
+
+ if (!gSendEncrypted || !bccRecipients.length || bccIsSender) {
+ if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+ return;
+ }
+
+ if (notification) {
+ return;
+ }
+
+ let ignoreButton = {
+ "l10n-id": "encrypted-bcc-ignore-button",
+ callback() {
+ gWantCannotEncryptBCCNotification = false;
+ return false;
+ },
+ };
+
+ gComposeNotification.appendNotification(
+ "warnEncryptedBccRecipients",
+ {
+ label: await document.l10n.formatValue("encrypted-bcc-warning"),
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ eventCallback(state) {
+ if (state == "dismissed") {
+ ignoreButton.callback();
+ }
+ },
+ },
+ [ignoreButton]
+ );
+}
+
+async function SendMessage() {
+ await pillifyRecipients();
+ let sendInBackground = Services.prefs.getBoolPref(
+ "mailnews.sendInBackground"
+ );
+ if (sendInBackground && AppConstants.platform != "macosx") {
+ let count = [...Services.wm.getEnumerator(null)].length;
+ if (count == 1) {
+ sendInBackground = false;
+ }
+ }
+
+ await GenericSendMessage(
+ sendInBackground
+ ? Ci.nsIMsgCompDeliverMode.Background
+ : Ci.nsIMsgCompDeliverMode.Now
+ );
+ ExitFullscreenMode();
+}
+
+async function SendMessageWithCheck() {
+ await pillifyRecipients();
+ var warn = Services.prefs.getBoolPref("mail.warn_on_send_accel_key");
+
+ if (warn) {
+ let bundle = getComposeBundle();
+ let checkValue = { value: false };
+ let buttonPressed = Services.prompt.confirmEx(
+ window,
+ bundle.getString("sendMessageCheckWindowTitle"),
+ bundle.getString("sendMessageCheckLabel"),
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1,
+ bundle.getString("sendMessageCheckSendButtonLabel"),
+ null,
+ null,
+ bundle.getString("CheckMsg"),
+ checkValue
+ );
+ if (buttonPressed != 0) {
+ return;
+ }
+ if (checkValue.value) {
+ Services.prefs.setBoolPref("mail.warn_on_send_accel_key", false);
+ }
+ }
+
+ let sendInBackground = Services.prefs.getBoolPref(
+ "mailnews.sendInBackground"
+ );
+
+ let mode;
+ if (Services.io.offline) {
+ mode = Ci.nsIMsgCompDeliverMode.Later;
+ } else {
+ mode = sendInBackground
+ ? Ci.nsIMsgCompDeliverMode.Background
+ : Ci.nsIMsgCompDeliverMode.Now;
+ }
+ await GenericSendMessage(mode);
+ ExitFullscreenMode();
+}
+
+async function SendMessageLater() {
+ await pillifyRecipients();
+ await GenericSendMessage(Ci.nsIMsgCompDeliverMode.Later);
+ ExitFullscreenMode();
+}
+
+function ExitFullscreenMode() {
+ // On OS X we need to deliberately exit full screen mode after sending.
+ if (AppConstants.platform == "macosx") {
+ window.fullScreen = false;
+ }
+}
+
+function Save() {
+ switch (defaultSaveOperation) {
+ case "file":
+ SaveAsFile(false);
+ break;
+ case "template":
+ SaveAsTemplate(false).catch(console.error);
+ break;
+ default:
+ SaveAsDraft(false).catch(console.error);
+ break;
+ }
+}
+
+function SaveAsFile(saveAs) {
+ GetCurrentEditorElement().contentDocument.title =
+ document.getElementById("msgSubject").value;
+
+ if (gMsgCompose.bodyConvertible() == Ci.nsIMsgCompConvertible.Plain) {
+ SaveDocument(saveAs, false, "text/plain");
+ } else {
+ SaveDocument(saveAs, false, "text/html");
+ }
+ defaultSaveOperation = "file";
+}
+
+async function SaveAsDraft() {
+ gAutoSaveKickedIn = false;
+ gEditingDraft = true;
+
+ await pillifyRecipients();
+ await GenericSendMessage(Ci.nsIMsgCompDeliverMode.SaveAsDraft);
+ defaultSaveOperation = "draft";
+}
+
+async function SaveAsTemplate() {
+ gAutoSaveKickedIn = false;
+ gEditingDraft = false;
+
+ await pillifyRecipients();
+ let savedReferences = null;
+ if (gMsgCompose && gMsgCompose.compFields) {
+ // Clear References header. When we use the template, we don't want that
+ // header, yet, "edit as new message" maintains it. So we need to clear
+ // it when saving the template.
+ // Note: The In-Reply-To header is the last entry in the references header,
+ // so it will get cleared as well.
+ savedReferences = gMsgCompose.compFields.references;
+ gMsgCompose.compFields.references = null;
+ }
+
+ await GenericSendMessage(Ci.nsIMsgCompDeliverMode.SaveAsTemplate);
+ defaultSaveOperation = "template";
+
+ if (savedReferences) {
+ gMsgCompose.compFields.references = savedReferences;
+ }
+}
+
+// Sets the additional FCC, in addition to the default FCC.
+function MessageFcc(aFolder) {
+ if (!gMsgCompose) {
+ return;
+ }
+
+ var msgCompFields = gMsgCompose.compFields;
+ if (!msgCompFields) {
+ return;
+ }
+
+ // Get the uri for the folder to FCC into.
+ var fccURI = aFolder.URI;
+ msgCompFields.fcc2 = msgCompFields.fcc2 == fccURI ? "nocopy://" : fccURI;
+}
+
+function updateOptionsMenu() {
+ setSecuritySettings("_Menubar");
+
+ let menuItem = document.getElementById("menu_inlineSpellCheck");
+ if (gSpellCheckingEnabled) {
+ menuItem.setAttribute("checked", "true");
+ } else {
+ menuItem.removeAttribute("checked");
+ }
+}
+
+function updatePriorityMenu() {
+ if (gMsgCompose) {
+ var msgCompFields = gMsgCompose.compFields;
+ if (msgCompFields && msgCompFields.priority) {
+ var priorityMenu = document.getElementById("priorityMenu");
+ priorityMenu.querySelector('[checked="true"]').removeAttribute("checked");
+ priorityMenu
+ .querySelector('[value="' + msgCompFields.priority + '"]')
+ .setAttribute("checked", "true");
+ }
+ }
+}
+
+function updatePriorityToolbarButton(newPriorityValue) {
+ var prioritymenu = document.getElementById("priorityMenu-button");
+ if (prioritymenu) {
+ prioritymenu.value = newPriorityValue;
+ }
+}
+
+function PriorityMenuSelect(target) {
+ if (gMsgCompose) {
+ var msgCompFields = gMsgCompose.compFields;
+ if (msgCompFields) {
+ msgCompFields.priority = target.getAttribute("value");
+ }
+
+ // keep priority toolbar button in synch with possible changes via the menu item
+ updatePriorityToolbarButton(target.getAttribute("value"));
+ }
+}
+
+/**
+ * Initialise the send format menu using the current gMsgCompose.compFields.
+ */
+function initSendFormatMenu() {
+ let formatToId = new Map([
+ [Ci.nsIMsgCompSendFormat.PlainText, "format_plain"],
+ [Ci.nsIMsgCompSendFormat.HTML, "format_html"],
+ [Ci.nsIMsgCompSendFormat.Both, "format_both"],
+ [Ci.nsIMsgCompSendFormat.Auto, "format_auto"],
+ ]);
+
+ let sendFormat = gMsgCompose.compFields.deliveryFormat;
+
+ if (sendFormat == Ci.nsIMsgCompSendFormat.Unset) {
+ sendFormat = Services.prefs.getIntPref(
+ "mail.default_send_format",
+ Ci.nsIMsgCompSendFormat.Auto
+ );
+
+ if (!formatToId.has(sendFormat)) {
+ // Unknown preference value.
+ sendFormat = Ci.nsIMsgCompSendFormat.Auto;
+ }
+ }
+
+ // Make the composition field uses the same as determined above. Specifically,
+ // if the deliveryFormat was Unset, we now set it to a specific value.
+ gMsgCompose.compFields.deliveryFormat = sendFormat;
+
+ for (let [format, id] of formatToId.entries()) {
+ let menuitem = document.getElementById(id);
+ menuitem.value = String(format);
+ if (format == sendFormat) {
+ menuitem.setAttribute("checked", "true");
+ } else {
+ menuitem.removeAttribute("checked");
+ }
+ }
+
+ document
+ .getElementById("outputFormatMenu")
+ .addEventListener("command", event => {
+ let prevSendFormat = gMsgCompose.compFields.deliveryFormat;
+ let newSendFormat = parseInt(event.target.value, 10);
+ gMsgCompose.compFields.deliveryFormat = newSendFormat;
+ gContentChanged = prevSendFormat != newSendFormat;
+ });
+}
+
+/**
+ * Walk through a plain text list of recipients and add them to the inline spell
+ * checker ignore list, e.g. to avoid that known recipient names get marked
+ * wrong in message body.
+ *
+ * @param {string} aAddressesToAdd - A (comma-separated) recipient(s) string.
+ */
+function addRecipientsToIgnoreList(aAddressesToAdd) {
+ if (gSpellCheckingEnabled) {
+ // break the list of potentially many recipients back into individual names
+ let addresses =
+ MailServices.headerParser.parseEncodedHeader(aAddressesToAdd);
+ let tokenizedNames = [];
+
+ // Each name could consist of multiple word delimited by either commas or spaces, i.e. Green Lantern
+ // or Lantern,Green. Tokenize on comma first, then tokenize again on spaces.
+ for (let addr of addresses) {
+ if (!addr.name) {
+ continue;
+ }
+ let splitNames = addr.name.split(",");
+ for (let i = 0; i < splitNames.length; i++) {
+ // now tokenize off of white space
+ let splitNamesFromWhiteSpaceArray = splitNames[i].split(" ");
+ for (
+ let whiteSpaceIndex = 0;
+ whiteSpaceIndex < splitNamesFromWhiteSpaceArray.length;
+ whiteSpaceIndex++
+ ) {
+ if (splitNamesFromWhiteSpaceArray[whiteSpaceIndex]) {
+ tokenizedNames.push(splitNamesFromWhiteSpaceArray[whiteSpaceIndex]);
+ }
+ }
+ }
+ }
+ spellCheckReadyObserver.addWordsToIgnore(tokenizedNames);
+ }
+}
+
+/**
+ * Observer waiting for spell checker to become initialized or to complete
+ * checking. When it fires, it pushes new words to be ignored to the speller.
+ */
+var spellCheckReadyObserver = {
+ _topic: "inlineSpellChecker-spellCheck-ended",
+
+ _ignoreWords: [],
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != this._topic) {
+ return;
+ }
+
+ this.removeObserver();
+ this._addWords();
+ },
+
+ _isAdded: false,
+
+ addObserver() {
+ if (this._isAdded) {
+ return;
+ }
+
+ Services.obs.addObserver(this, this._topic);
+ this._isAdded = true;
+ },
+
+ removeObserver() {
+ if (!this._isAdded) {
+ return;
+ }
+
+ Services.obs.removeObserver(this, this._topic);
+ this._clearPendingWords();
+ this._isAdded = false;
+ },
+
+ addWordsToIgnore(aIgnoreWords) {
+ this._ignoreWords.push(...aIgnoreWords);
+ let checker = GetCurrentEditorSpellChecker();
+ if (!checker || checker.spellCheckPending) {
+ // spellchecker is enabled, but we must wait for its init to complete
+ this.addObserver();
+ } else {
+ this._addWords();
+ }
+ },
+
+ _addWords() {
+ // At the time the speller finally got initialized, we may already be closing
+ // the compose together with the speller, so we need to check if they
+ // are still valid.
+ let checker = GetCurrentEditorSpellChecker();
+ if (gMsgCompose && checker?.enableRealTimeSpell) {
+ checker.ignoreWords(this._ignoreWords);
+ }
+ this._clearPendingWords();
+ },
+
+ _clearPendingWords() {
+ this._ignoreWords.length = 0;
+ },
+};
+
+/**
+ * Called if the list of recipients changed in any way.
+ *
+ * @param {boolean} automatic - Set to true if the change of recipients was
+ * invoked programmatically and should not be considered a change of message
+ * content.
+ */
+function onRecipientsChanged(automatic) {
+ if (!automatic) {
+ gContentChanged = true;
+ }
+ updateSendCommands(true);
+}
+
+/**
+ * Show the popup identified by aPopupID
+ * at the anchor element identified by aAnchorID.
+ *
+ * Note: All but the first 2 parameters are identical with the parameters of
+ * the openPopup() method of XUL popup element. For details, please consult docs.
+ * Except aPopupID, all parameters are optional.
+ * Example: showPopupById("aPopupID", "aAnchorID");
+ *
+ * @param aPopupID the ID of the popup element to be shown
+ * @param aAnchorID the ID of an element to which the popup should be anchored
+ * @param aPosition a single-word alignment value for the position parameter
+ * of openPopup() method; defaults to "after_start" if omitted.
+ * @param x x offset from default position
+ * @param y y offset from default position
+ * @param isContextMenu {boolean} For details, see documentation.
+ * @param attributesOverride {boolean} whether the position attribute on the
+ * popup node overrides the position parameter
+ * @param triggerEvent the event that triggered the popup
+ */
+function showPopupById(
+ aPopupID,
+ aAnchorID,
+ aPosition = "after_start",
+ x,
+ y,
+ isContextMenu,
+ attributesOverride,
+ triggerEvent
+) {
+ let popup = document.getElementById(aPopupID);
+ let anchor = document.getElementById(aAnchorID);
+ popup.openPopup(
+ anchor,
+ aPosition,
+ x,
+ y,
+ isContextMenu,
+ attributesOverride,
+ triggerEvent
+ );
+}
+
+function InitLanguageMenu() {
+ var languageMenuList = document.getElementById("languageMenuList");
+ if (!languageMenuList) {
+ return;
+ }
+
+ var spellChecker = Cc["@mozilla.org/spellchecker/engine;1"].getService(
+ Ci.mozISpellCheckingEngine
+ );
+
+ // Get the list of dictionaries from
+ // the spellchecker.
+
+ var dictList = spellChecker.getDictionaryList();
+
+ let extraItemCount = dictList.length === 0 ? 1 : 2;
+
+ // If dictionary count hasn't changed then no need to update the menu.
+ if (dictList.length + extraItemCount == languageMenuList.childElementCount) {
+ return;
+ }
+
+ var sortedList = gSpellChecker.sortDictionaryList(dictList);
+
+ let getMoreItem = document.createXULElement("menuitem");
+ document.l10n.setAttributes(getMoreItem, "spell-add-dictionaries");
+ getMoreItem.addEventListener("command", event => {
+ event.stopPropagation();
+ openDictionaryList();
+ });
+ let getMoreArray = [getMoreItem];
+
+ if (extraItemCount > 1) {
+ getMoreArray.unshift(document.createXULElement("menuseparator"));
+ }
+
+ // Remove any languages from the list.
+ languageMenuList.replaceChildren(
+ ...sortedList.map(dict => {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("label", dict.displayName);
+ item.setAttribute("value", dict.localeCode);
+ item.setAttribute("type", "checkbox");
+ item.setAttribute("selection-type", "multiple");
+ if (dictList.length > 1) {
+ item.setAttribute("closemenu", "none");
+ }
+ return item;
+ }),
+ ...getMoreArray
+ );
+}
+
+function OnShowDictionaryMenu(aTarget) {
+ InitLanguageMenu();
+
+ for (let item of aTarget.children) {
+ item.setAttribute(
+ "checked",
+ gActiveDictionaries.has(item.getAttribute("value"))
+ );
+ }
+}
+
+function languageMenuListOpened() {
+ document
+ .getElementById("languageStatusButton")
+ .setAttribute("aria-expanded", "true");
+}
+
+function languageMenuListClosed() {
+ document
+ .getElementById("languageStatusButton")
+ .setAttribute("aria-expanded", "false");
+}
+
+/**
+ * Set of the active dictionaries. We maintain this cached state so we don't
+ * need a spell checker instance to know the active dictionaries. This is
+ * especially relevant when inline spell checking is disabled.
+ *
+ * @type {Set<string>}
+ */
+var gActiveDictionaries = new Set();
+/**
+ * Change the language of the composition and if we are using inline
+ * spell check, recheck the message with the new dictionary.
+ *
+ * Note: called from the "Check Spelling" panel in SelectLanguage().
+ *
+ * @param {string[]} languages - New languages to set.
+ */
+async function ComposeChangeLanguage(languages) {
+ let currentLanguage = document.documentElement.getAttribute("lang");
+ if (
+ (languages.length === 1 && currentLanguage != languages[0]) ||
+ languages.length !== 1
+ ) {
+ let languageToSet = "";
+ if (languages.length === 1) {
+ languageToSet = languages[0];
+ }
+ // Update the document language as well.
+ document.documentElement.setAttribute("lang", languageToSet);
+ }
+
+ await gSpellChecker?.selectDictionaries(languages);
+
+ let checker = GetCurrentEditorSpellChecker();
+ if (checker?.spellChecker) {
+ await checker.spellChecker.setCurrentDictionaries(languages);
+ }
+ // Update subject spell checker languages. If for some reason the spell
+ // checker isn't ready yet, don't auto-create it, hence pass 'false'.
+ let subjectSpellChecker = checker?.spellChecker
+ ? document.getElementById("msgSubject").editor.getInlineSpellChecker(false)
+ : null;
+ if (subjectSpellChecker?.spellChecker) {
+ await subjectSpellChecker.spellChecker.setCurrentDictionaries(languages);
+ }
+
+ // now check the document over again with the new dictionary
+ if (gSpellCheckingEnabled) {
+ if (checker?.spellChecker) {
+ checker.spellCheckRange(null);
+ }
+
+ if (subjectSpellChecker?.spellChecker) {
+ // Also force a recheck of the subject.
+ subjectSpellChecker.spellCheckRange(null);
+ }
+ }
+
+ await updateLanguageInStatusBar(languages);
+
+ // Update the language in the composition fields, so we can save it
+ // to the draft next time.
+ if (gMsgCompose?.compFields) {
+ let langs = "";
+ if (!Services.prefs.getBoolPref("mail.suppress_content_language")) {
+ langs = languages.join(", ");
+ }
+ gMsgCompose.compFields.contentLanguage = langs;
+ }
+
+ gActiveDictionaries = new Set(languages);
+
+ // Notify compose WebExtension API about changed dictionaries.
+ window.dispatchEvent(
+ new CustomEvent("active-dictionaries-changed", {
+ detail: languages.join(","),
+ })
+ );
+}
+
+/**
+ * Change the language of the composition and if we are using inline
+ * spell check, recheck the message with the new dictionary.
+ *
+ * @param {Event} event - Event of selecting an item in the spelling button
+ * menulist popup.
+ */
+function ChangeLanguage(event) {
+ let curLangs = new Set(gActiveDictionaries);
+ if (curLangs.has(event.target.value)) {
+ curLangs.delete(event.target.value);
+ } else {
+ curLangs.add(event.target.value);
+ }
+ ComposeChangeLanguage(Array.from(curLangs));
+ event.stopPropagation();
+}
+
+/**
+ * Update the active dictionaries in the status bar.
+ *
+ * @param {string[]} dictionaries
+ */
+async function updateLanguageInStatusBar(dictionaries) {
+ // HACK: calling sortDictionaryList (in InitLanguageMenu) may fail the first
+ // time due to synchronous loading of the .ftl files. If we load the files
+ // and wait for a known value asynchronously, no such failure will happen.
+ await new Localization([
+ "toolkit/intl/languageNames.ftl",
+ "toolkit/intl/regionNames.ftl",
+ ]).formatValue("language-name-en");
+
+ InitLanguageMenu();
+ let languageMenuList = document.getElementById("languageMenuList");
+ let languageStatusButton = document.getElementById("languageStatusButton");
+ if (!languageMenuList || !languageStatusButton) {
+ return;
+ }
+
+ if (!dictionaries) {
+ dictionaries = Array.from(gActiveDictionaries);
+ }
+ let listFormat = new Intl.ListFormat(undefined, {
+ type: "conjunction",
+ style: "short",
+ });
+ let languages = [];
+ let item = languageMenuList.firstElementChild;
+
+ // No status display, if there is only one or no spelling dictionary available.
+ if (languageMenuList.childElementCount <= 3) {
+ languageStatusButton.hidden = true;
+ languageStatusButton.textContent = "";
+ return;
+ }
+
+ languageStatusButton.hidden = false;
+ while (item) {
+ if (item.tagName.toLowerCase() === "menuseparator") {
+ break;
+ }
+ if (dictionaries.includes(item.getAttribute("value"))) {
+ languages.push(item.getAttribute("label"));
+ }
+ item = item.nextElementSibling;
+ }
+ if (languages.length > 0) {
+ languageStatusButton.textContent = listFormat.format(languages);
+ } else {
+ languageStatusButton.textContent = listFormat.format(dictionaries);
+ }
+}
+
+/**
+ * Toggle Return Receipt (Disposition-Notification-To: header).
+ *
+ * @param {boolean} [forcedState] - Forced state to use for returnReceipt.
+ * If not set, the current state will be toggled.
+ */
+function ToggleReturnReceipt(forcedState) {
+ let msgCompFields = gMsgCompose.compFields;
+ if (!msgCompFields) {
+ return;
+ }
+ if (forcedState === undefined) {
+ msgCompFields.returnReceipt = !msgCompFields.returnReceipt;
+ gReceiptOptionChanged = true;
+ } else {
+ if (msgCompFields.returnReceipt != forcedState) {
+ gReceiptOptionChanged = true;
+ }
+ msgCompFields.returnReceipt = forcedState;
+ }
+ for (let item of document.querySelectorAll(`menuitem[command="cmd_toggleReturnReceipt"],
+ toolbarbutton[command="cmd_toggleReturnReceipt"]`)) {
+ item.setAttribute("checked", msgCompFields.returnReceipt);
+ }
+}
+
+function ToggleDSN(target) {
+ let msgCompFields = gMsgCompose.compFields;
+ if (msgCompFields) {
+ msgCompFields.DSN = !msgCompFields.DSN;
+ target.setAttribute("checked", msgCompFields.DSN);
+ gDSNOptionChanged = true;
+ }
+}
+
+function ToggleAttachVCard(target) {
+ var msgCompFields = gMsgCompose.compFields;
+ if (msgCompFields) {
+ msgCompFields.attachVCard = !msgCompFields.attachVCard;
+ target.setAttribute("checked", msgCompFields.attachVCard);
+ gAttachVCardOptionChanged = true;
+ }
+}
+
+/**
+ * Toggles or sets the status of manual Attachment Reminder, i.e. whether
+ * the user will get the "Attachment Reminder" alert before sending or not.
+ * Toggles checkmark on "Remind me later" menuitem and internal
+ * gManualAttachmentReminder flag accordingly.
+ *
+ * @param aState (optional) true = activate reminder.
+ * false = deactivate reminder.
+ * (default) = toggle reminder state.
+ */
+function toggleAttachmentReminder(aState = !gManualAttachmentReminder) {
+ gManualAttachmentReminder = aState;
+ document.getElementById("cmd_remindLater").setAttribute("checked", aState);
+ gMsgCompose.compFields.attachmentReminder = aState;
+
+ // If we enabled manual reminder, the reminder can't be turned off.
+ if (aState) {
+ gDisableAttachmentReminder = false;
+ }
+
+ manageAttachmentNotification(false);
+}
+
+/**
+ * Triggers or removes the CSS animation for the counter of newly uploaded
+ * attachments.
+ */
+function toggleAttachmentAnimation() {
+ gAttachmentCounter.classList.toggle("is_animating");
+}
+
+function FillIdentityList(menulist) {
+ let accounts = FolderUtils.allAccountsSorted(true);
+
+ let accountHadSeparator = false;
+ let firstAccountWithIdentities = true;
+ for (let account of accounts) {
+ let identities = account.identities;
+
+ if (identities.length == 0) {
+ continue;
+ }
+
+ let needSeparator = identities.length > 1;
+ if (needSeparator || accountHadSeparator) {
+ // Separate identities from this account from the previous
+ // account's identities if there is more than 1 in the current
+ // or previous account.
+ if (!firstAccountWithIdentities) {
+ // only if this is not the first account shown
+ let separator = document.createXULElement("menuseparator");
+ menulist.menupopup.appendChild(separator);
+ }
+ accountHadSeparator = needSeparator;
+ }
+ firstAccountWithIdentities = false;
+
+ for (let i = 0; i < identities.length; i++) {
+ let identity = identities[i];
+ let item = menulist.appendItem(
+ identity.identityName,
+ identity.fullAddress,
+ account.incomingServer.prettyName
+ );
+ item.setAttribute("identitykey", identity.key);
+ item.setAttribute("accountkey", account.key);
+ if (i == 0) {
+ // Mark the first identity as default.
+ item.setAttribute("default", "true");
+ }
+ // Create the menuitem description and add it after the last label in the
+ // menuitem internals.
+ let desc = document.createXULElement("label");
+ desc.value = item.getAttribute("description");
+ desc.classList.add("menu-description");
+ desc.setAttribute("crop", "end");
+ item.querySelector("label:last-child").after(desc);
+ }
+ }
+
+ menulist.menupopup.appendChild(document.createXULElement("menuseparator"));
+ menulist.menupopup
+ .appendChild(document.createXULElement("menuitem"))
+ .setAttribute("command", "cmd_customizeFromAddress");
+}
+
+function getCurrentAccountKey() {
+ // Get the account's key.
+ let identityList = document.getElementById("msgIdentity");
+ return identityList.getAttribute("accountkey");
+}
+
+function getCurrentIdentityKey() {
+ // Get the identity key.
+ return gCurrentIdentity.key;
+}
+
+function AdjustFocus() {
+ // If is NNTP account, check the newsgroup field.
+ let account = MailServices.accounts.getAccount(getCurrentAccountKey());
+ let accountType = account.incomingServer.type;
+
+ let element =
+ accountType == "nntp"
+ ? document.getElementById("newsgroupsAddrContainer")
+ : document.getElementById("toAddrContainer");
+
+ // Focus on the recipient input field if no pills are present.
+ if (element.querySelectorAll("mail-address-pill").length == 0) {
+ element.querySelector(".address-row-input").focus();
+ return;
+ }
+
+ // Focus subject if empty.
+ element = document.getElementById("msgSubject");
+ if (element.value == "") {
+ element.focus();
+ return;
+ }
+
+ // Focus message body.
+ focusMsgBody();
+}
+
+/**
+ * Set the compose window title with flavors (Write | Print Preview).
+ *
+ * @param isPrintPreview (optional) true: Set title for 'Print Preview' window.
+ * false: Set title for 'Write' window (default).
+ */
+function SetComposeWindowTitle(isPrintPreview = false) {
+ let aStringName = isPrintPreview
+ ? "windowTitlePrintPreview"
+ : "windowTitleWrite";
+ let subject =
+ document.getElementById("msgSubject").value.trim() ||
+ getComposeBundle().getString("defaultSubject");
+ let brandBundle = document.getElementById("brandBundle");
+ let brandShortName = brandBundle.getString("brandShortName");
+ let newTitle = getComposeBundle().getFormattedString(aStringName, [
+ subject,
+ brandShortName,
+ ]);
+ document.title = newTitle;
+ if (AppConstants.platform == "macosx") {
+ document.getElementById("titlebar-title-label").value = newTitle;
+ }
+}
+
+// Check for changes to document and allow saving before closing
+// This is hooked up to the OS's window close widget (e.g., "X" for Windows)
+function ComposeCanClose() {
+ // No open compose window?
+ if (!gMsgCompose) {
+ return true;
+ }
+
+ // Do this early, so ldap sessions have a better chance to
+ // cleanup after themselves.
+ if (gSendOperationInProgress || gSaveOperationInProgress) {
+ let result;
+
+ let brandBundle = document.getElementById("brandBundle");
+ let brandShortName = brandBundle.getString("brandShortName");
+ let promptTitle = gSendOperationInProgress
+ ? getComposeBundle().getString("quitComposeWindowTitle")
+ : getComposeBundle().getString("quitComposeWindowSaveTitle");
+ let promptMsg = gSendOperationInProgress
+ ? getComposeBundle().getFormattedString(
+ "quitComposeWindowMessage2",
+ [brandShortName],
+ 1
+ )
+ : getComposeBundle().getFormattedString(
+ "quitComposeWindowSaveMessage",
+ [brandShortName],
+ 1
+ );
+ let quitButtonLabel = getComposeBundle().getString(
+ "quitComposeWindowQuitButtonLabel2"
+ );
+ let waitButtonLabel = getComposeBundle().getString(
+ "quitComposeWindowWaitButtonLabel2"
+ );
+
+ result = Services.prompt.confirmEx(
+ window,
+ promptTitle,
+ promptMsg,
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1,
+ waitButtonLabel,
+ quitButtonLabel,
+ null,
+ null,
+ { value: 0 }
+ );
+
+ if (result == 1) {
+ gMsgCompose.abort();
+ return true;
+ }
+ return false;
+ }
+
+ // Returns FALSE only if user cancels save action
+ if (
+ gContentChanged ||
+ gMsgCompose.bodyModified ||
+ gAutoSaveKickedIn ||
+ gReceiptOptionChanged ||
+ gDSNOptionChanged
+ ) {
+ // call window.focus, since we need to pop up a dialog
+ // and therefore need to be visible (to prevent user confusion)
+ window.focus();
+ let draftFolderURI = gCurrentIdentity.draftFolder;
+ let draftFolderName =
+ MailUtils.getOrCreateFolder(draftFolderURI).prettyName;
+ let result = Services.prompt.confirmEx(
+ window,
+ getComposeBundle().getString("saveDlogTitle"),
+ getComposeBundle().getFormattedString("saveDlogMessages3", [
+ draftFolderName,
+ ]),
+ Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2,
+ null,
+ null,
+ getComposeBundle().getString("discardButtonLabel"),
+ null,
+ { value: 0 }
+ );
+ switch (result) {
+ case 0: // Save
+ // Since we're going to save the message, we tell toolkit that
+ // the close command failed, by returning false, and then
+ // we close the window ourselves after the save is done.
+ gCloseWindowAfterSave = true;
+ // We catch the exception because we need to tell toolkit that it
+ // shouldn't close the window, because we're going to close it
+ // ourselves. If we don't tell toolkit that, and then close the window
+ // ourselves, the toolkit code that keeps track of the open windows
+ // gets off by one and the app can close unexpectedly on os's that
+ // shutdown the app when the last window is closed.
+ GenericSendMessage(Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft).catch(
+ console.error
+ );
+ return false;
+ case 1: // Cancel
+ return false;
+ case 2: // Don't Save
+ // don't delete the draft if we didn't start off editing a draft
+ // and the user hasn't explicitly saved it.
+ if (!gEditingDraft && gAutoSaveKickedIn) {
+ RemoveDraft();
+ }
+ // Remove auto-saved draft created during "edit template".
+ if (gMsgCompose.compFields.templateId && gAutoSaveKickedIn) {
+ RemoveDraft();
+ }
+ break;
+ }
+ }
+
+ return true;
+}
+
+function RemoveDraft() {
+ try {
+ var draftUri = gMsgCompose.compFields.draftId;
+ var msgKey = draftUri.substr(draftUri.indexOf("#") + 1);
+ let folder = MailUtils.getExistingFolder(gMsgCompose.savedFolderURI);
+ if (!folder) {
+ return;
+ }
+ try {
+ if (folder.getFlag(Ci.nsMsgFolderFlags.Drafts)) {
+ let msgHdr = folder.GetMessageHeader(msgKey);
+ folder.deleteMessages([msgHdr], null, true, false, null, false);
+ }
+ } catch (ex) {
+ // couldn't find header - perhaps an imap folder.
+ var imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (imapFolder) {
+ imapFolder.storeImapFlags(
+ Ci.nsMsgFolderFlags.Expunged,
+ true,
+ [msgKey],
+ null
+ );
+ }
+ }
+ } catch (ex) {}
+}
+
+function SetContentAndBodyAsUnmodified() {
+ gMsgCompose.bodyModified = false;
+ gContentChanged = false;
+}
+
+function MsgComposeCloseWindow() {
+ if (gMsgCompose) {
+ gMsgCompose.CloseWindow();
+ } else {
+ window.close();
+ }
+}
+
+function GetLastAttachDirectory() {
+ var lastDirectory;
+
+ try {
+ lastDirectory = Services.prefs.getComplexValue(
+ kComposeAttachDirPrefName,
+ Ci.nsIFile
+ );
+ } catch (ex) {
+ // this will fail the first time we attach a file
+ // as we won't have a pref value.
+ lastDirectory = null;
+ }
+
+ return lastDirectory;
+}
+
+// attachedLocalFile must be a nsIFile
+function SetLastAttachDirectory(attachedLocalFile) {
+ try {
+ let file = attachedLocalFile.QueryInterface(Ci.nsIFile);
+ let parent = file.parent.QueryInterface(Ci.nsIFile);
+
+ Services.prefs.setComplexValue(
+ kComposeAttachDirPrefName,
+ Ci.nsIFile,
+ parent
+ );
+ } catch (ex) {
+ dump("error: SetLastAttachDirectory failed: " + ex + "\n");
+ }
+}
+
+function AttachFile() {
+ if (gAttachmentBucket.itemCount) {
+ // If there are existing attachments already, restore attachment pane before
+ // showing the file picker so that user can see them while adding more.
+ toggleAttachmentPane("show");
+ }
+
+ // Get file using nsIFilePicker and convert to URL
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(
+ window,
+ getComposeBundle().getString("chooseFileToAttach"),
+ Ci.nsIFilePicker.modeOpenMultiple
+ );
+
+ let lastDirectory = GetLastAttachDirectory();
+ if (lastDirectory) {
+ fp.displayDirectory = lastDirectory;
+ }
+
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.files) {
+ return;
+ }
+
+ let file;
+ let attachments = [];
+
+ for (file of [...fp.files]) {
+ attachments.push(FileToAttachment(file));
+ }
+
+ AddAttachments(attachments);
+ SetLastAttachDirectory(file);
+ });
+}
+
+/**
+ * Convert an nsIFile instance into an nsIMsgAttachment.
+ *
+ * @param file the nsIFile
+ * @returns an attachment pointing to the file
+ */
+function FileToAttachment(file) {
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+
+ attachment.url = fileHandler.getURLSpecFromActualFile(file);
+ attachment.size = file.fileSize;
+ return attachment;
+}
+
+async function messageAttachmentToFile(attachment) {
+ let pathTempDir = PathUtils.join(
+ PathUtils.tempDir,
+ "pid-" + Services.appinfo.processID
+ );
+ await IOUtils.makeDirectory(pathTempDir, { permissions: 0o700 });
+ let pathTempFile = await IOUtils.createUniqueFile(
+ pathTempDir,
+ attachment.name.replaceAll(/[/:*?\"<>|]/g, "_"),
+ 0o600
+ );
+ let tempFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ tempFile.initWithPath(pathTempFile);
+ let extAppLauncher = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsPIExternalAppLauncher);
+ extAppLauncher.deleteTemporaryFileOnExit(tempFile);
+
+ let service = MailServices.messageServiceFromURI(attachment.url);
+ let bytes = await new Promise((resolve, reject) => {
+ let streamlistener = {
+ _data: [],
+ _stream: null,
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ if (!this._stream) {
+ this._stream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ this._stream.init(aInputStream);
+ }
+ this._data.push(this._stream.read(aCount));
+ },
+ onStartRequest() {},
+ onStopRequest(aRequest, aStatus) {
+ if (aStatus == Cr.NS_OK) {
+ resolve(this._data.join(""));
+ } else {
+ console.error(aStatus);
+ reject();
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+ };
+
+ service.streamMessage(
+ attachment.url,
+ streamlistener,
+ null, // aMsgWindow
+ null, // aUrlListener
+ false, // aConvertData
+ "" //aAdditionalHeader
+ );
+ });
+ await IOUtils.write(
+ pathTempFile,
+ lazy.MailStringUtils.byteStringToUint8Array(bytes)
+ );
+ return tempFile;
+}
+
+/**
+ * Add a list of attachment objects as attachments. The attachment URLs must
+ * be set.
+ *
+ * @param {nsIMsgAttachment[]} aAttachments - Objects to add as attachments.
+ * @param {boolean} [aContentChanged=true] - Optional value to assign gContentChanged
+ * after adding attachments.
+ */
+async function AddAttachments(aAttachments, aContentChanged = true) {
+ let addedAttachments = [];
+ let items = [];
+
+ for (let attachment of aAttachments) {
+ if (!attachment?.url || DuplicateFileAlreadyAttached(attachment)) {
+ continue;
+ }
+
+ if (!attachment.name) {
+ attachment.name = gMsgCompose.AttachmentPrettyName(attachment.url, null);
+ }
+
+ // For security reasons, don't allow *-message:// uris to leak out.
+ // We don't want to reveal the .slt path (for mailbox://), or the username
+ // or hostname.
+ // Don't allow file or mail/news protocol uris to leak out either.
+ if (
+ /^mailbox-message:|^imap-message:|^news-message:/i.test(attachment.name)
+ ) {
+ attachment.name = getComposeBundle().getString(
+ "messageAttachmentSafeName"
+ );
+ } else if (/^file:|^mailbox:|^imap:|^s?news:/i.test(attachment.name)) {
+ attachment.name = getComposeBundle().getString("partAttachmentSafeName");
+ }
+
+ // Create temporary files for message attachments.
+ if (
+ /^mailbox-message:|^imap-message:|^news-message:/i.test(attachment.url)
+ ) {
+ try {
+ let messageFile = await messageAttachmentToFile(attachment);
+ // Store the original mailbox:// url in contentLocation.
+ attachment.contentLocation = attachment.url;
+ attachment.url = Services.io.newFileURI(messageFile).spec;
+ } catch (ex) {
+ console.error(
+ `Could not save message attachment ${attachment.url} as file: ${ex}`
+ );
+ }
+ }
+
+ if (
+ attachment.msgUri &&
+ /^mailbox-message:|^imap-message:|^news-message:/i.test(
+ attachment.msgUri
+ ) &&
+ attachment.url &&
+ /^mailbox:|^imap:|^s?news:/i.test(attachment.url)
+ ) {
+ // This is an attachment of another message, create a temporary file and
+ // update the url.
+ let pathTempDir = PathUtils.join(
+ PathUtils.tempDir,
+ "pid-" + Services.appinfo.processID
+ );
+ await IOUtils.makeDirectory(pathTempDir, { permissions: 0o700 });
+ let tempDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ tempDir.initWithPath(pathTempDir);
+
+ let tempFile = gMessenger.saveAttachmentToFolder(
+ attachment.contentType,
+ attachment.url,
+ encodeURIComponent(attachment.name),
+ attachment.msgUri,
+ tempDir
+ );
+ let extAppLauncher = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsPIExternalAppLauncher);
+ extAppLauncher.deleteTemporaryFileOnExit(tempFile);
+ // Store the original mailbox:// url in contentLocation.
+ attachment.contentLocation = attachment.url;
+ attachment.url = Services.io.newFileURI(tempFile).spec;
+ }
+
+ let item = gAttachmentBucket.appendItem(attachment);
+ addedAttachments.push(attachment);
+
+ let tooltiptext;
+ try {
+ tooltiptext = decodeURI(attachment.url);
+ } catch {
+ tooltiptext = attachment.url;
+ }
+ item.setAttribute("tooltiptext", tooltiptext);
+ item.addEventListener("command", OpenSelectedAttachment);
+ items.push(item);
+ }
+
+ if (addedAttachments.length > 0) {
+ // Trigger a visual feedback to let the user know how many attachments have
+ // been added.
+ gAttachmentCounter.textContent = `+${addedAttachments.length}`;
+ toggleAttachmentAnimation();
+
+ // Move the focus on the last attached file so the user can see a visual
+ // feedback of what was added.
+ gAttachmentBucket.selectedIndex = gAttachmentBucket.getIndexOfItem(
+ items[items.length - 1]
+ );
+
+ // Ensure the selected item is visible and if not the box will scroll to it.
+ gAttachmentBucket.ensureIndexIsVisible(gAttachmentBucket.selectedIndex);
+
+ AttachmentsChanged("show", aContentChanged);
+ dispatchAttachmentBucketEvent("attachments-added", addedAttachments);
+
+ // Set min height for the attachment bucket.
+ if (!gAttachmentBucket.style.minHeight) {
+ // Min height is the height of the first child plus padding and border.
+ // Note: we assume the computed styles have px values.
+ let bucketStyle = getComputedStyle(gAttachmentBucket);
+ let childStyle = getComputedStyle(gAttachmentBucket.firstChild);
+ let minHeight =
+ gAttachmentBucket.firstChild.getBoundingClientRect().height +
+ parseFloat(childStyle.marginBlockStart) +
+ parseFloat(childStyle.marginBlockEnd) +
+ parseFloat(bucketStyle.paddingBlockStart) +
+ parseFloat(bucketStyle.paddingBlockEnd) +
+ parseFloat(bucketStyle.borderBlockStartWidth) +
+ parseFloat(bucketStyle.borderBlockEndWidth);
+ gAttachmentBucket.style.minHeight = `${minHeight}px`;
+ }
+ }
+
+ // Always show the attachment pane if we have any attachment, to prevent
+ // keeping the panel collapsed when the user interacts with the attachment
+ // button.
+ if (gAttachmentBucket.itemCount) {
+ toggleAttachmentPane("show");
+ }
+
+ return items;
+}
+
+/**
+ * Returns a sorted-by-index, "non-live" array of attachment list items.
+ *
+ * @param aAscending {boolean}: true (default): sort return array ascending
+ * false : sort return array descending
+ * @param aSelectedOnly {boolean}: true: return array of selected items only.
+ * false (default): return array of all items.
+ *
+ * @returns {Array} an array of (all | selected) listItem elements in
+ * attachmentBucket listbox, "non-live" and sorted by their index
+ * in the list; [] if there are (no | no selected) attachments.
+ */
+function attachmentsGetSortedArray(aAscending = true, aSelectedOnly = false) {
+ let listItems;
+
+ if (aSelectedOnly) {
+ // Selected attachments only.
+ if (!gAttachmentBucket.selectedCount) {
+ return [];
+ }
+
+ // gAttachmentBucket.selectedItems is a "live" and "unordered" node list
+ // (items get added in the order they were added to the selection). But we
+ // want a stable ("non-live") array of selected items, sorted by their index
+ // in the list.
+ listItems = [...gAttachmentBucket.selectedItems];
+ } else {
+ // All attachments.
+ if (!gAttachmentBucket.itemCount) {
+ return [];
+ }
+
+ listItems = [...gAttachmentBucket.itemChildren];
+ }
+
+ if (aAscending) {
+ listItems.sort(
+ (a, b) =>
+ gAttachmentBucket.getIndexOfItem(a) -
+ gAttachmentBucket.getIndexOfItem(b)
+ );
+ } else {
+ // descending
+ listItems.sort(
+ (a, b) =>
+ gAttachmentBucket.getIndexOfItem(b) -
+ gAttachmentBucket.getIndexOfItem(a)
+ );
+ }
+ return listItems;
+}
+
+/**
+ * Returns a sorted-by-index, "non-live" array of selected attachment list items.
+ *
+ * @param aAscending {boolean}: true (default): sort return array ascending
+ * false : sort return array descending
+ * @returns {Array} an array of selected listitem elements in attachmentBucket
+ * listbox, "non-live" and sorted by their index in the list;
+ * [] if no attachments selected
+ */
+function attachmentsSelectionGetSortedArray(aAscending = true) {
+ return attachmentsGetSortedArray(aAscending, true);
+}
+
+/**
+ * Return true if the selected attachment items are a coherent block in the list,
+ * otherwise false.
+ *
+ * @param aListPosition (optional) - "top" : Return true only if the block is
+ * at the top of the list.
+ * "bottom": Return true only if the block is
+ * at the bottom of the list.
+ * @returns {boolean} true : The selected attachment items are a coherent block
+ * (at the list edge if/as specified by 'aListPosition'),
+ * or only 1 item selected.
+ * false: The selected attachment items are NOT a coherent block
+ * (at the list edge if/as specified by 'aListPosition'),
+ * or no attachments selected, or no attachments,
+ * or no attachmentBucket.
+ */
+function attachmentsSelectionIsBlock(aListPosition) {
+ if (!gAttachmentBucket.selectedCount) {
+ // No attachments selected, no attachments, or no attachmentBucket.
+ return false;
+ }
+
+ let selItems = attachmentsSelectionGetSortedArray();
+ let indexFirstSelAttachment = gAttachmentBucket.getIndexOfItem(selItems[0]);
+ let indexLastSelAttachment = gAttachmentBucket.getIndexOfItem(
+ selItems[gAttachmentBucket.selectedCount - 1]
+ );
+ let isBlock =
+ indexFirstSelAttachment ==
+ indexLastSelAttachment + 1 - gAttachmentBucket.selectedCount;
+
+ switch (aListPosition) {
+ case "top":
+ // True if selection is a coherent block at the top of the list.
+ return indexFirstSelAttachment == 0 && isBlock;
+ case "bottom":
+ // True if selection is a coherent block at the bottom of the list.
+ return (
+ indexLastSelAttachment == gAttachmentBucket.itemCount - 1 && isBlock
+ );
+ default:
+ // True if selection is a coherent block.
+ return isBlock;
+ }
+}
+
+function AttachPage() {
+ let result = { value: "http://" };
+ if (
+ Services.prompt.prompt(
+ window,
+ getComposeBundle().getString("attachPageDlogTitle"),
+ getComposeBundle().getString("attachPageDlogMessage"),
+ result,
+ null,
+ { value: 0 }
+ )
+ ) {
+ if (result.value.length <= "http://".length) {
+ // Nothing filled, just show the dialog again.
+ AttachPage();
+ return;
+ }
+
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.url = result.value;
+ AddAttachments([attachment]);
+ }
+}
+
+/**
+ * Check if the given attachment already exists in the attachment bucket.
+ *
+ * @param nsIMsgAttachment - the attachment to check
+ * @returns true if the attachment is already attached
+ */
+function DuplicateFileAlreadyAttached(attachment) {
+ for (let item of gAttachmentBucket.itemChildren) {
+ if (item.attachment && item.attachment.url) {
+ if (item.attachment.url == attachment.url) {
+ return true;
+ }
+ // Also check, if an attachment has been saved as a temporary file and its
+ // original url is a match.
+ if (
+ item.attachment.contentLocation &&
+ item.attachment.contentLocation == attachment.url
+ ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function Attachments2CompFields(compFields) {
+ // First, we need to clear all attachment in the compose fields.
+ compFields.removeAttachments();
+
+ for (let item of gAttachmentBucket.itemChildren) {
+ if (item.attachment) {
+ compFields.addAttachment(item.attachment);
+ }
+ }
+}
+
+async function RemoveAllAttachments() {
+ // Ensure that attachment pane is shown before removing all attachments.
+ toggleAttachmentPane("show");
+
+ if (!gAttachmentBucket.itemCount) {
+ return;
+ }
+
+ await RemoveAttachments(gAttachmentBucket.itemChildren);
+}
+
+/**
+ * Show or hide the attachment pane after updating its header bar information
+ * (number and total file size of attachments) and tooltip.
+ *
+ * @param aShowBucket {Boolean} true: show the attachment pane
+ * false (or omitted): hide the attachment pane
+ */
+function UpdateAttachmentBucket(aShowBucket) {
+ updateAttachmentPane(aShowBucket ? "show" : "hide");
+}
+
+/**
+ * Update the header bar information (number and total file size of attachments)
+ * and tooltip of attachment pane, then (optionally) show or hide the pane.
+ *
+ * @param aShowPane {string} "show": show the attachment pane
+ * "hide": hide the attachment pane
+ * omitted: just update without changing pane visibility
+ */
+function updateAttachmentPane(aShowPane) {
+ let count = gAttachmentBucket.itemCount;
+
+ document.l10n.setAttributes(
+ document.getElementById("attachmentBucketCount"),
+ "attachment-bucket-count-value",
+ {
+ count,
+ }
+ );
+
+ let attachmentsSize = 0;
+ for (let item of gAttachmentBucket.itemChildren) {
+ gAttachmentBucket.invalidateItem(item);
+ attachmentsSize += item.cloudHtmlFileSize
+ ? item.cloudHtmlFileSize
+ : item.attachment.size;
+ }
+
+ document.getElementById("attachmentBucketSize").textContent =
+ count > 0 ? gMessenger.formatFileSize(attachmentsSize) : "";
+
+ document
+ .getElementById("composeContentBox")
+ .classList.toggle("attachment-area-hidden", !count);
+
+ attachmentBucketUpdateTooltips();
+
+ // If aShowPane argument is omitted, it's just updating, so we're done.
+ if (aShowPane === undefined) {
+ return;
+ }
+
+ // Otherwise, show or hide the panel per aShowPane argument.
+ toggleAttachmentPane(aShowPane);
+}
+
+async function RemoveSelectedAttachment() {
+ if (!gAttachmentBucket.selectedCount) {
+ return;
+ }
+
+ await RemoveAttachments(gAttachmentBucket.selectedItems);
+}
+
+/**
+ * Removes the provided attachmentItems from the composer and deletes all
+ * associated cloud files.
+ *
+ * Note: Cloud file delete errors are not considered to be fatal errors. They do
+ * not prevent the attachments from being removed from the composer. Such
+ * errors are caught and logged to the console.
+ *
+ * @param {DOMNode[]} items - AttachmentItems to be removed
+ */
+async function RemoveAttachments(items) {
+ // Remember the current focus index so we can try to restore it when done.
+ let focusIndex = gAttachmentBucket.currentIndex;
+
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ let removedAttachments = [];
+
+ let promises = [];
+ for (let i = items.length - 1; i >= 0; i--) {
+ let item = items[i];
+
+ if (item.attachment.sendViaCloud && item.cloudFileAccount) {
+ if (item.uploading) {
+ let file = fileHandler.getFileFromURLSpec(item.attachment.url);
+ promises.push(
+ item.uploading
+ .cancelFileUpload(window, file)
+ .catch(ex => console.warn(ex.message))
+ );
+ } else {
+ promises.push(
+ item.cloudFileAccount
+ .deleteFile(window, item.cloudFileUpload.id)
+ .catch(ex => console.warn(ex.message))
+ );
+ }
+ }
+
+ removedAttachments.push(item.attachment);
+ // Let's release the attachment object held by the node else it won't go
+ // away until the window is destroyed
+ item.attachment = null;
+ item.remove();
+ }
+
+ if (removedAttachments.length > 0) {
+ // Bug 1661507 workaround: Force update of selectedCount and selectedItem,
+ // both wrong after item removal, to avoid confusion for listening command
+ // controllers.
+ gAttachmentBucket.clearSelection();
+
+ AttachmentsChanged();
+ dispatchAttachmentBucketEvent("attachments-removed", removedAttachments);
+ }
+
+ // Collapse the attachment container if all the items have been deleted.
+ if (!gAttachmentBucket.itemCount) {
+ toggleAttachmentPane("hide");
+ } else {
+ // Try to restore the original focused item or somewhere close by.
+ gAttachmentBucket.currentIndex =
+ focusIndex < gAttachmentBucket.itemCount
+ ? focusIndex
+ : gAttachmentBucket.itemCount - 1;
+ }
+
+ await Promise.all(promises);
+}
+
+async function RenameSelectedAttachment() {
+ if (gAttachmentBucket.selectedItems.length != 1) {
+ // Not one attachment selected.
+ return;
+ }
+
+ let item = gAttachmentBucket.getSelectedItem(0);
+ let originalName = item.attachment.name;
+ let attachmentName = { value: originalName };
+ if (
+ Services.prompt.prompt(
+ window,
+ getComposeBundle().getString("renameAttachmentTitle"),
+ getComposeBundle().getString("renameAttachmentMessage"),
+ attachmentName,
+ null,
+ { value: 0 }
+ )
+ ) {
+ if (attachmentName.value == "" || attachmentName.value == originalName) {
+ // Name was not filled nor changed, bail out.
+ return;
+ }
+ try {
+ await UpdateAttachment(item, {
+ name: attachmentName.value,
+ relatedCloudFileUpload: item.CloudFileUpload,
+ });
+ } catch (ex) {
+ showLocalizedCloudFileAlert(ex);
+ }
+ }
+}
+
+/* eslint-disable complexity */
+/**
+ * Move selected attachment(s) within the attachment list.
+ *
+ * @param {string} aDirection - The direction in which to move the attachments.
+ * "left" : Move attachments left in the list.
+ * "right" : Move attachments right in the list.
+ * "top" : Move attachments to the top of the list.
+ * "bottom" : Move attachments to the bottom of the list.
+ * "bundleUp" : Move attachments together (upwards).
+ * "bundleDown": Move attachments together (downwards).
+ * "toggleSort": Sort attachments alphabetically (toggle).
+ */
+function moveSelectedAttachments(aDirection) {
+ // Command controllers will bail out if no or all attachments are selected,
+ // or if block selections can't be moved, or if other direction-specific
+ // adverse circumstances prevent the intended movement.
+ if (!aDirection) {
+ return;
+ }
+
+ // Ensure focus on gAttachmentBucket when we're coming from
+ // 'Reorder Attachments' panel.
+ gAttachmentBucket.focus();
+
+ // Get a sorted and "non-live" array of gAttachmentBucket.selectedItems.
+ let selItems = attachmentsSelectionGetSortedArray();
+
+ // In case of misspelled aDirection.
+ let visibleIndex = gAttachmentBucket.currentIndex;
+ // Keep track of the item we had focused originally. Deselect it though,
+ // since listbox gets confused if you move its focused item around.
+ let focusItem = gAttachmentBucket.currentItem;
+ gAttachmentBucket.currentItem = null;
+ let upwards;
+ let targetItem;
+
+ switch (aDirection) {
+ case "left":
+ case "right":
+ // Move selected attachments upwards/downwards.
+ upwards = aDirection == "left";
+ let blockItems = [];
+
+ for (let item of selItems) {
+ // Handle adjacent selected items en block, via blockItems array.
+ blockItems.push(item); // Add current selItem to blockItems.
+ let nextItem = item.nextElementSibling;
+ if (!nextItem || !nextItem.selected) {
+ // If current selItem is the last blockItem, check out its adjacent
+ // item in the intended direction to see if there's room for moving.
+ // Note that the block might contain one or more items.
+ let checkItem = upwards
+ ? blockItems[0].previousElementSibling
+ : nextItem;
+ // If block-adjacent checkItem exists (and is not selected because
+ // then it would be part of the block), we can move the block to the
+ // right position.
+ if (checkItem) {
+ targetItem = upwards
+ ? // Upwards: Insert block items before checkItem,
+ // i.e. before previousElementSibling of block.
+ checkItem
+ : // Downwards: Insert block items *after* checkItem,
+ // i.e. *before* nextElementSibling.nextElementSibling of block,
+ // which works according to spec even if that's null.
+ checkItem.nextElementSibling;
+ // Move current blockItems.
+ for (let blockItem of blockItems) {
+ gAttachmentBucket.insertBefore(blockItem, targetItem);
+ }
+ }
+ // Else if checkItem doesn't exist, the block is already at the edge
+ // of the list, so we can't move it in the intended direction.
+ blockItems.length = 0; // Either way, we're done with the current block.
+ }
+ // Else if current selItem is NOT the end of the current block, proceed:
+ // Add next selItem to the block and see if that's the end of the block.
+ } // Next selItem.
+
+ // Ensure helpful visibility of moved items (scroll into view if needed):
+ // If first item of selection is now at the top, first list item.
+ // Else if last item of selection is now at the bottom, last list item.
+ // Otherwise, let's see where we are going by ensuring visibility of the
+ // nearest unselected sibling of selection according to direction of move.
+ if (gAttachmentBucket.getIndexOfItem(selItems[0]) == 0) {
+ visibleIndex = 0;
+ } else if (
+ gAttachmentBucket.getIndexOfItem(selItems[selItems.length - 1]) ==
+ gAttachmentBucket.itemCount - 1
+ ) {
+ visibleIndex = gAttachmentBucket.itemCount - 1;
+ } else if (upwards) {
+ visibleIndex = gAttachmentBucket.getIndexOfItem(
+ selItems[0].previousElementSibling
+ );
+ } else {
+ visibleIndex = gAttachmentBucket.getIndexOfItem(
+ selItems[selItems.length - 1].nextElementSibling
+ );
+ }
+ break;
+
+ case "top":
+ case "bottom":
+ case "bundleUp":
+ case "bundleDown":
+ // Bundle selected attachments to top/bottom of the list or upwards/downwards.
+
+ upwards = ["top", "bundleUp"].includes(aDirection);
+ // Downwards: Reverse order of selItems so we can use the same algorithm.
+ if (!upwards) {
+ selItems.reverse();
+ }
+
+ if (["top", "bottom"].includes(aDirection)) {
+ let listEdgeItem = gAttachmentBucket.getItemAtIndex(
+ upwards ? 0 : gAttachmentBucket.itemCount - 1
+ );
+ let selEdgeItem = selItems[0];
+ if (selEdgeItem != listEdgeItem) {
+ // Top/Bottom: Move the first/last selected item to the edge of the list
+ // so that we always have an initial anchor target block in the right
+ // place, so we can use the same algorithm for top/bottom and
+ // inner bundling.
+ targetItem = upwards
+ ? // Upwards: Insert before first list item.
+ listEdgeItem
+ : // Downwards: Insert after last list item, i.e.
+ // *before* non-existing listEdgeItem.nextElementSibling,
+ // which is null. It works because it's a feature.
+ null;
+ gAttachmentBucket.insertBefore(selEdgeItem, targetItem);
+ }
+ }
+ // We now have a selected block (at least one item) at the target position.
+ // Let's find the end (inner edge) of that block and move only the
+ // remaining selected items to avoid unnecessary moves.
+ targetItem = null;
+ for (let item of selItems) {
+ if (targetItem) {
+ // We know where to move it, so move it!
+ gAttachmentBucket.insertBefore(item, targetItem);
+ if (!upwards) {
+ // Downwards: As selItems are reversed, and there's no insertAfter()
+ // method to insert *after* a stable target, we need to insert
+ // *before* the first item of the target block at target position,
+ // which is the current selItem which we've just moved onto the block.
+ targetItem = item;
+ }
+ } else {
+ // If there's no targetItem yet, find the inner edge of the target block.
+ let nextItem = upwards
+ ? item.nextElementSibling
+ : item.previousElementSibling;
+ if (!nextItem.selected) {
+ // If nextItem is not selected, current selItem is the inner edge of
+ // the initial anchor target block, so we can set targetItem.
+ targetItem = upwards
+ ? // Upwards: set stable targetItem.
+ nextItem
+ : // Downwards: set initial targetItem.
+ item;
+ }
+ // Else if nextItem is selected, it is still part of initial anchor
+ // target block, so just proceed to look for the edge of that block.
+ }
+ } // next selItem
+
+ // Ensure visibility of first/last selected item after the move.
+ visibleIndex = gAttachmentBucket.getIndexOfItem(selItems[0]);
+ break;
+
+ case "toggleSort":
+ // Sort the selected attachments alphabetically after moving them together.
+ // The command updater of cmd_sortAttachmentsToggle toggles the sorting
+ // direction based on the current sorting and block status of the selection.
+
+ let toggleCmd = document.getElementById("cmd_sortAttachmentsToggle");
+ let sortDirection =
+ toggleCmd.getAttribute("sortdirection") || "ascending";
+ let sortItems;
+ let sortSelection;
+
+ if (gAttachmentBucket.selectedCount > 1) {
+ // Sort selected attachments only.
+ sortSelection = true;
+ sortItems = selItems;
+ // Move selected attachments together before sorting as a block.
+ goDoCommand("cmd_moveAttachmentBundleUp");
+
+ // Find the end of the selected block to find our targetItem.
+ for (let item of selItems) {
+ let nextItem = item.nextElementSibling;
+ if (!nextItem || !nextItem.selected) {
+ // If there's no nextItem (block at list bottom), or nextItem is
+ // not selected, we've reached the end of the block.
+ // Set the block's nextElementSibling as targetItem and exit loop.
+ // Works by definition even if nextElementSibling aka nextItem is null.
+ targetItem = nextItem;
+ break;
+ }
+ // else if (nextItem && nextItem.selected), nextItem is still part of
+ // the block, so proceed with checking its nextElementSibling.
+ } // next selItem
+ } else {
+ // Sort all attachments.
+ sortSelection = false;
+ sortItems = attachmentsGetSortedArray();
+ targetItem = null; // Insert at the end of the list.
+ }
+ // Now let's sort our sortItems according to sortDirection.
+ if (sortDirection == "ascending") {
+ sortItems.sort((a, b) =>
+ a.attachment.name.localeCompare(b.attachment.name)
+ );
+ } else {
+ // "descending"
+ sortItems.sort((a, b) =>
+ b.attachment.name.localeCompare(a.attachment.name)
+ );
+ }
+
+ // Insert sortItems in new order before the nextElementSibling of the block.
+ for (let item of sortItems) {
+ gAttachmentBucket.insertBefore(item, targetItem);
+ }
+
+ if (sortSelection) {
+ // After sorting selection: Ensure visibility of first selected item.
+ visibleIndex = gAttachmentBucket.getIndexOfItem(selItems[0]);
+ } else {
+ // After sorting all items: Ensure visibility of selected item,
+ // otherwise first list item.
+ visibleIndex =
+ selItems.length == 1 ? gAttachmentBucket.selectedIndex : 0;
+ }
+ break;
+ } // end switch (aDirection)
+
+ // Restore original focus.
+ gAttachmentBucket.currentItem = focusItem;
+ // Ensure smart visibility of a relevant item according to direction.
+ gAttachmentBucket.ensureIndexIsVisible(visibleIndex);
+
+ // Moving selected items around does not trigger auto-updating of our command
+ // handlers, so we must do it now as the position of selected items has changed.
+ updateReorderAttachmentsItems();
+}
+/* eslint-enable complexity */
+
+/**
+ * Toggle attachment pane view state: show or hide it.
+ * If aAction parameter is omitted, toggle current view state.
+ *
+ * @param {string} [aAction = "toggle"] - "show": show attachment pane
+ * "hide": hide attachment pane
+ * "toggle": toggle attachment pane
+ */
+function toggleAttachmentPane(aAction = "toggle") {
+ let attachmentArea = document.getElementById("attachmentArea");
+
+ if (aAction == "toggle") {
+ // Interrupt if we don't have any attachment as we don't want nor need to
+ // show an empty container.
+ if (!gAttachmentBucket.itemCount) {
+ return;
+ }
+
+ if (attachmentArea.open && document.activeElement != gAttachmentBucket) {
+ // Interrupt and move the focus to the attachment pane if it's already
+ // visible but not currently focused.
+ moveFocusToAttachmentPane();
+ return;
+ }
+
+ // Toggle attachment pane.
+ attachmentArea.open = !attachmentArea.open;
+ } else {
+ attachmentArea.open = aAction != "hide";
+ }
+}
+
+/**
+ * Update the #attachmentArea according to its open state.
+ */
+function attachmentAreaOnToggle() {
+ let attachmentArea = document.getElementById("attachmentArea");
+ let bucketHasFocus = document.activeElement == gAttachmentBucket;
+ if (attachmentArea.open && !bucketHasFocus) {
+ moveFocusToAttachmentPane();
+ } else if (!attachmentArea.open && bucketHasFocus) {
+ // Move the focus to the message body only if the bucket was focused.
+ focusMsgBody();
+ }
+
+ // Make the splitter non-interactive whilst the bucket is hidden.
+ document
+ .getElementById("composeContentBox")
+ .classList.toggle("attachment-bucket-closed", !attachmentArea.open);
+
+ // Update the checkmark on menuitems hooked up with cmd_toggleAttachmentPane.
+ // Menuitem does not have .checked property nor .toggleAttribute(), sigh.
+ for (let menuitem of document.querySelectorAll(
+ 'menuitem[command="cmd_toggleAttachmentPane"]'
+ )) {
+ if (attachmentArea.open) {
+ menuitem.setAttribute("checked", "true");
+ continue;
+ }
+ menuitem.removeAttribute("checked");
+ }
+
+ // Update the title based on the collapsed status of the bucket.
+ document.l10n.setAttributes(
+ attachmentArea.querySelector("summary"),
+ attachmentArea.open ? "attachment-area-hide" : "attachment-area-show"
+ );
+}
+
+/**
+ * Ensure the focus is properly moved to the Attachment Bucket, and to the first
+ * available item if present.
+ */
+function moveFocusToAttachmentPane() {
+ gAttachmentBucket.focus();
+
+ if (gAttachmentBucket.currentItem) {
+ gAttachmentBucket.ensureElementIsVisible(gAttachmentBucket.currentItem);
+ }
+}
+
+function showReorderAttachmentsPanel() {
+ // Ensure attachment pane visibility as it might be collapsed.
+ toggleAttachmentPane("show");
+ showPopupById(
+ "reorderAttachmentsPanel",
+ "attachmentBucket",
+ "after_start",
+ 15,
+ 0
+ );
+ // After the panel is shown, focus attachmentBucket so that keyboard
+ // operation for selecting and moving attachment items works; the panel
+ // helpfully presents the keyboard shortcuts for moving things around.
+ // Bucket focus is also required because the panel will only close with ESC
+ // or attachmentBucketOnBlur(), and that's because we're using noautohide as
+ // event.preventDefault() of onpopuphiding event fails when the panel
+ // is auto-hiding, but we don't want panel to hide when focus goes to bucket.
+ gAttachmentBucket.focus();
+}
+
+/**
+ * Returns a string representing the current sort order of selected attachment
+ * items by their names. We don't check if selected items form a coherent block
+ * or not; use attachmentsSelectionIsBlock() to check on that.
+ *
+ * @returns {string} "ascending" : Sort order is ascending.
+ * "descending": Sort order is descending.
+ * "equivalent": The names of all selected items are equivalent.
+ * "" : There's no sort order, or only 1 item selected,
+ * or no items selected, or no attachments,
+ * or no attachmentBucket.
+ */
+function attachmentsSelectionGetSortOrder() {
+ return attachmentsGetSortOrder(true);
+}
+
+/**
+ * Returns a string representing the current sort order of attachment items
+ * by their names.
+ *
+ * @param aSelectedOnly {boolean}: true: return sort order of selected items only.
+ * false (default): return sort order of all items.
+ *
+ * @returns {string} "ascending" : Sort order is ascending.
+ * "descending": Sort order is descending.
+ * "equivalent": The names of the items are equivalent.
+ * "" : There's no sort order, or no attachments,
+ * or no attachmentBucket; or (with aSelectedOnly),
+ * only 1 item selected, or no items selected.
+ */
+function attachmentsGetSortOrder(aSelectedOnly = false) {
+ let listItems;
+ if (aSelectedOnly) {
+ if (gAttachmentBucket.selectedCount <= 1) {
+ return "";
+ }
+
+ listItems = attachmentsSelectionGetSortedArray();
+ } else {
+ // aSelectedOnly == false
+ if (!gAttachmentBucket.itemCount) {
+ return "";
+ }
+
+ listItems = attachmentsGetSortedArray();
+ }
+
+ // We're comparing each item to the next item, so exclude the last item.
+ let listItems1 = listItems.slice(0, -1);
+ let someAscending;
+ let someDescending;
+
+ // Check if some adjacent items are sorted ascending.
+ someAscending = listItems1.some(
+ (item, index) =>
+ item.attachment.name.localeCompare(listItems[index + 1].attachment.name) <
+ 0
+ );
+
+ // Check if some adjacent items are sorted descending.
+ someDescending = listItems1.some(
+ (item, index) =>
+ item.attachment.name.localeCompare(listItems[index + 1].attachment.name) >
+ 0
+ );
+
+ // Unsorted (but not all equivalent in sort order)
+ if (someAscending && someDescending) {
+ return "";
+ }
+
+ if (someAscending && !someDescending) {
+ return "ascending";
+ }
+
+ if (someDescending && !someAscending) {
+ return "descending";
+ }
+
+ // No ascending pairs, no descending pairs, so all equivalent in sort order.
+ // if (!someAscending && !someDescending)
+ return "equivalent";
+}
+
+function reorderAttachmentsPanelOnPopupShowing() {
+ let panel = document.getElementById("reorderAttachmentsPanel");
+ let buttonsNodeList = panel.querySelectorAll(".panelButton");
+ let buttons = [...buttonsNodeList]; // convert NodeList to Array
+ // Let's add some pretty keyboard shortcuts to the buttons.
+ buttons.forEach(btn => {
+ if (btn.hasAttribute("key")) {
+ btn.setAttribute("prettykey", getPrettyKey(btn.getAttribute("key")));
+ }
+ });
+ // Focus attachment bucket to activate attachmentBucketController, which is
+ // required for updating the reorder commands.
+ gAttachmentBucket.focus();
+ // We're updating commands before showing the panel so that button states
+ // don't change after the panel is shown, and also because focus is still
+ // in attachment bucket right now, which is required for updating them.
+ updateReorderAttachmentsItems();
+}
+
+function attachmentHeaderContextOnPopupShowing() {
+ let initiallyShowItem = document.getElementById(
+ "attachmentHeaderContext_initiallyShowItem"
+ );
+
+ initiallyShowItem.setAttribute(
+ "checked",
+ Services.prefs.getBoolPref("mail.compose.show_attachment_pane")
+ );
+}
+
+function toggleInitiallyShowAttachmentPane(aMenuItem) {
+ Services.prefs.setBoolPref(
+ "mail.compose.show_attachment_pane",
+ aMenuItem.getAttribute("checked")
+ );
+}
+
+/**
+ * Handle blur event on attachment pane and control visibility of
+ * reorderAttachmentsPanel.
+ */
+function attachmentBucketOnBlur() {
+ let reorderAttachmentsPanel = document.getElementById(
+ "reorderAttachmentsPanel"
+ );
+ // If attachment pane has really lost focus, and if reorderAttachmentsPanel is
+ // not currently in the process of showing up, hide reorderAttachmentsPanel.
+ // Otherwise, keep attachments selected and the reorderAttachmentsPanel open
+ // when reordering and after renaming via dialog.
+ if (
+ document.activeElement.id != "attachmentBucket" &&
+ reorderAttachmentsPanel.state != "showing"
+ ) {
+ reorderAttachmentsPanel.hidePopup();
+ }
+}
+
+/**
+ * Handle the keypress on the attachment bucket.
+ *
+ * @param {Event} event - The keypress DOM Event.
+ */
+function attachmentBucketOnKeyPress(event) {
+ // Interrupt if the Alt modifier is pressed, meaning the user is reordering
+ // the list of attachments.
+ if (event.altKey) {
+ return;
+ }
+
+ switch (event.key) {
+ case "Escape":
+ let reorderAttachmentsPanel = document.getElementById(
+ "reorderAttachmentsPanel"
+ );
+
+ // Close the reorderAttachmentsPanel if open and interrupt.
+ if (reorderAttachmentsPanel.state == "open") {
+ reorderAttachmentsPanel.hidePopup();
+ return;
+ }
+
+ if (gAttachmentBucket.itemCount) {
+ // Deselect selected items in a full bucket if any.
+ if (gAttachmentBucket.selectedCount) {
+ gAttachmentBucket.clearSelection();
+ return;
+ }
+
+ // Move the focus to the message body.
+ focusMsgBody();
+ return;
+ }
+
+ // Close an empty bucket.
+ toggleAttachmentPane("hide");
+ break;
+
+ case "Enter":
+ // Enter on empty bucket to add file attachments, convenience
+ // keyboard equivalent of single-click on bucket whitespace.
+ if (!gAttachmentBucket.itemCount) {
+ goDoCommand("cmd_attachFile");
+ }
+ break;
+
+ case "ArrowLeft":
+ gAttachmentBucket.moveByOffset(-1, !event.ctrlKey, event.shiftKey);
+ event.preventDefault();
+ break;
+
+ case "ArrowRight":
+ gAttachmentBucket.moveByOffset(1, !event.ctrlKey, event.shiftKey);
+ event.preventDefault();
+ break;
+
+ case "ArrowDown":
+ gAttachmentBucket.moveByOffset(
+ gAttachmentBucket._itemsPerRow(),
+ !event.ctrlKey,
+ event.shiftKey
+ );
+ event.preventDefault();
+ break;
+
+ case "ArrowUp":
+ gAttachmentBucket.moveByOffset(
+ -gAttachmentBucket._itemsPerRow(),
+ !event.ctrlKey,
+ event.shiftKey
+ );
+
+ event.preventDefault();
+ break;
+ }
+}
+
+function attachmentBucketOnClick(aEvent) {
+ // Handle click on attachment pane whitespace normally clear selection.
+ // If there are no attachments in the bucket, show 'Attach File(s)' dialog.
+ if (
+ aEvent.button == 0 &&
+ aEvent.target.getAttribute("is") == "attachment-list" &&
+ !aEvent.target.firstElementChild
+ ) {
+ goDoCommand("cmd_attachFile");
+ }
+}
+
+function attachmentBucketOnSelect() {
+ attachmentBucketUpdateTooltips();
+ updateAttachmentItems();
+}
+
+function attachmentBucketUpdateTooltips() {
+ // Attachment pane whitespace tooltip
+ if (gAttachmentBucket.selectedCount) {
+ gAttachmentBucket.tooltipText = getComposeBundle().getString(
+ "attachmentBucketClearSelectionTooltip"
+ );
+ } else {
+ gAttachmentBucket.tooltipText = getComposeBundle().getString(
+ "attachmentBucketAttachFilesTooltip"
+ );
+ }
+}
+
+function OpenSelectedAttachment() {
+ if (gAttachmentBucket.selectedItems.length != 1) {
+ return;
+ }
+ let attachment = gAttachmentBucket.getSelectedItem(0).attachment;
+ let attachmentUrl = attachment.url;
+
+ let messagePrefix = /^mailbox-message:|^imap-message:|^news-message:/i;
+ if (messagePrefix.test(attachmentUrl)) {
+ // we must be dealing with a forwarded attachment, treat this special
+ let msgHdr =
+ MailServices.messageServiceFromURI(attachmentUrl).messageURIToMsgHdr(
+ attachmentUrl
+ );
+ if (msgHdr) {
+ MailUtils.openMessageInNewWindow(msgHdr);
+ }
+ return;
+ }
+ if (
+ attachment.contentType == "application/pdf" ||
+ /\.pdf$/i.test(attachment.name)
+ ) {
+ // @see msgHdrView.js which has simililar opening functionality
+ let handlerInfo = gMIMEService.getFromTypeAndExtension(
+ attachment.contentType,
+ attachment.name.split(".").pop()
+ );
+ // Only open a new tab for pdfs if we are handling them internally.
+ if (
+ !handlerInfo.alwaysAskBeforeHandling &&
+ handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally
+ ) {
+ // Add the content type to avoid a "how do you want to open this?"
+ // dialog. The type may already be there, but that doesn't matter.
+ let url = attachment.url;
+ if (!url.includes("type=")) {
+ url += url.includes("?") ? "&" : "?";
+ url += "type=application/pdf";
+ }
+ let tabmail = Services.wm
+ .getMostRecentWindow("mail:3pane")
+ ?.document.getElementById("tabmail");
+ if (tabmail) {
+ tabmail.openTab("contentTab", {
+ url,
+ background: false,
+ linkHandler: "single-page",
+ });
+ tabmail.ownerGlobal.focus();
+ return;
+ }
+ // If no tabmail, open PDF same as other attachments.
+ }
+ }
+ let uri = Services.io.newURI(attachmentUrl);
+ let channel = Services.io.newChannelFromURI(
+ uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ let uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
+ uriLoader.openURI(channel, true, new nsAttachmentOpener());
+}
+
+function nsAttachmentOpener() {}
+
+nsAttachmentOpener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIURIContentListener",
+ "nsIInterfaceRequestor",
+ ]),
+
+ doContent(contentType, isContentPreferred, request, contentHandler) {
+ // If we came here to display an attached message, make sure we provide a type.
+ if (/[?&]part=/i.test(request.URI.query)) {
+ let newQuery = request.URI.query + "&type=message/rfc822";
+ request.URI = request.URI.mutate().setQuery(newQuery).finalize();
+ }
+ let newHandler = Cc[
+ "@mozilla.org/uriloader/content-handler;1?type=application/x-message-display"
+ ].createInstance(Ci.nsIContentHandler);
+ newHandler.handleContent("application/x-message-display", this, request);
+ return true;
+ },
+
+ isPreferred(contentType, desiredContentType) {
+ if (contentType == "message/rfc822") {
+ return true;
+ }
+ return false;
+ },
+
+ canHandleContent(contentType, isContentPreferred, desiredContentType) {
+ return false;
+ },
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIDOMWindow)) {
+ return window;
+ }
+ if (iid.equals(Ci.nsIDocShell)) {
+ return window.docShell;
+ }
+ return this.QueryInterface(iid);
+ },
+
+ loadCookie: null,
+ parentContentListener: null,
+};
+
+/**
+ * Determine the sending format depending on the selected format, or the content
+ * of the message body.
+ *
+ * @returns {nsIMsgCompSendFormat} The determined send format: either PlainText,
+ * HTML or Both (never Auto or Unset).
+ */
+function determineSendFormat() {
+ if (!gMsgCompose.composeHTML) {
+ return Ci.nsIMsgCompSendFormat.PlainText;
+ }
+
+ let sendFormat = gMsgCompose.compFields.deliveryFormat;
+ if (sendFormat != Ci.nsIMsgCompSendFormat.Auto) {
+ return sendFormat;
+ }
+
+ // Auto downgrade if safe to do so.
+ let convertible;
+ try {
+ convertible = gMsgCompose.bodyConvertible();
+ } catch (ex) {
+ return Ci.nsIMsgCompSendFormat.Both;
+ }
+ return convertible == Ci.nsIMsgCompConvertible.Plain
+ ? Ci.nsIMsgCompSendFormat.PlainText
+ : Ci.nsIMsgCompSendFormat.Both;
+}
+
+/**
+ * Expands mailinglists found in the recipient fields.
+ */
+function expandRecipients() {
+ gMsgCompose.expandMailingLists();
+}
+
+/**
+ * Hides addressing options (To, CC, Bcc, Newsgroup, Followup-To, etc.)
+ * that are not relevant for the account type used for sending.
+ *
+ * @param {string} accountKey - Key of the account that is currently selected
+ * as the sending account.
+ * @param {string} prevKey - Key of the account that was previously selected
+ * as the sending account.
+ */
+function hideIrrelevantAddressingOptions(accountKey, prevKey) {
+ let showNews = false;
+ for (let account of MailServices.accounts.accounts) {
+ if (account.incomingServer.type == "nntp") {
+ showNews = true;
+ }
+ }
+ // If there is no News (NNTP) account existing then
+ // hide the Newsgroup and Followup-To recipient type menuitems.
+ for (let item of document.querySelectorAll(".news-show-row-menuitem")) {
+ showAddressRowMenuItemSetVisibility(item, showNews);
+ }
+
+ let account = MailServices.accounts.getAccount(accountKey);
+ let accountType = account.incomingServer.type;
+
+ // If the new account is a News (NNTP) account.
+ if (accountType == "nntp") {
+ updateUIforNNTPAccount();
+ return;
+ }
+
+ // If the new account is a Mail account and a previous account was selected.
+ if (accountType != "nntp" && prevKey != "") {
+ updateUIforMailAccount();
+ }
+}
+
+function LoadIdentity(startup) {
+ let identityElement = document.getElementById("msgIdentity");
+ let prevIdentity = gCurrentIdentity;
+
+ let idKey = null;
+ let accountKey = null;
+ let prevKey = getCurrentAccountKey();
+ if (identityElement.selectedItem) {
+ // Set the identity key value on the menu list.
+ idKey = identityElement.selectedItem.getAttribute("identitykey");
+ identityElement.setAttribute("identitykey", idKey);
+ gCurrentIdentity = MailServices.accounts.getIdentity(idKey);
+
+ // Set the account key value on the menu list.
+ accountKey = identityElement.selectedItem.getAttribute("accountkey");
+ identityElement.setAttribute("accountkey", accountKey);
+
+ // Update the addressing options only if a new account was selected.
+ if (prevKey != getCurrentAccountKey()) {
+ hideIrrelevantAddressingOptions(accountKey, prevKey);
+ }
+ }
+ for (let input of document.querySelectorAll(".mail-input,.news-input")) {
+ let params = JSON.parse(input.searchParam);
+ params.idKey = idKey;
+ params.accountKey = accountKey;
+ input.searchParam = JSON.stringify(params);
+ }
+
+ if (startup) {
+ // During compose startup, bail out here.
+ return;
+ }
+
+ // Since switching the signature loses the caret position, we record it
+ // and restore it later.
+ let editor = GetCurrentEditor();
+ let selection = editor.selection;
+ let range = selection.getRangeAt(0);
+ let start = range.startOffset;
+ let startNode = range.startContainer;
+
+ editor.enableUndo(false);
+
+ // Handle non-startup changing of identity.
+ if (prevIdentity && idKey != prevIdentity.key) {
+ let changedRecipients = false;
+ let prevReplyTo = prevIdentity.replyTo;
+ let prevCc = "";
+ let prevBcc = "";
+ let prevReceipt = prevIdentity.requestReturnReceipt;
+ let prevDSN = prevIdentity.DSN;
+ let prevAttachVCard = prevIdentity.attachVCard;
+
+ if (prevIdentity.doCc && prevIdentity.doCcList) {
+ prevCc += prevIdentity.doCcList;
+ }
+
+ if (prevIdentity.doBcc && prevIdentity.doBccList) {
+ prevBcc += prevIdentity.doBccList;
+ }
+
+ let newReplyTo = gCurrentIdentity.replyTo;
+ let newCc = "";
+ let newBcc = "";
+ let newReceipt = gCurrentIdentity.requestReturnReceipt;
+ let newDSN = gCurrentIdentity.DSN;
+ let newAttachVCard = gCurrentIdentity.attachVCard;
+
+ if (gCurrentIdentity.doCc && gCurrentIdentity.doCcList) {
+ newCc += gCurrentIdentity.doCcList;
+ }
+
+ if (gCurrentIdentity.doBcc && gCurrentIdentity.doBccList) {
+ newBcc += gCurrentIdentity.doBccList;
+ }
+
+ let msgCompFields = gMsgCompose.compFields;
+ // Update recipients in msgCompFields to match pills currently in the UI.
+ Recipients2CompFields(msgCompFields);
+
+ if (
+ !gReceiptOptionChanged &&
+ prevReceipt == msgCompFields.returnReceipt &&
+ prevReceipt != newReceipt
+ ) {
+ msgCompFields.returnReceipt = newReceipt;
+ ToggleReturnReceipt(msgCompFields.returnReceipt);
+ }
+
+ if (
+ !gDSNOptionChanged &&
+ prevDSN == msgCompFields.DSN &&
+ prevDSN != newDSN
+ ) {
+ msgCompFields.DSN = newDSN;
+ document
+ .getElementById("dsnMenu")
+ .setAttribute("checked", msgCompFields.DSN);
+ }
+
+ if (
+ !gAttachVCardOptionChanged &&
+ prevAttachVCard == msgCompFields.attachVCard &&
+ prevAttachVCard != newAttachVCard
+ ) {
+ msgCompFields.attachVCard = newAttachVCard;
+ document
+ .getElementById("cmd_attachVCard")
+ .setAttribute("checked", msgCompFields.attachVCard);
+ }
+
+ if (newReplyTo != prevReplyTo) {
+ if (prevReplyTo != "") {
+ awRemoveRecipients(msgCompFields, "addr_reply", prevReplyTo);
+ }
+ if (newReplyTo != "") {
+ awAddRecipients(msgCompFields, "addr_reply", newReplyTo);
+ }
+ }
+
+ let toCcAddrs = new Set([
+ ...msgCompFields.splitRecipients(msgCompFields.to, true),
+ ...msgCompFields.splitRecipients(msgCompFields.cc, true),
+ ]);
+
+ if (newCc != prevCc) {
+ if (prevCc) {
+ awRemoveRecipients(msgCompFields, "addr_cc", prevCc);
+ }
+ if (newCc) {
+ // Add only Auto-Cc recipients whose email is not already in To or CC.
+ newCc = msgCompFields
+ .splitRecipients(newCc, false)
+ .filter(
+ x => !toCcAddrs.has(...msgCompFields.splitRecipients(x, true))
+ )
+ .join(", ");
+ awAddRecipients(msgCompFields, "addr_cc", newCc);
+ }
+ changedRecipients = true;
+ }
+
+ if (newBcc != prevBcc) {
+ let toCcBccAddrs = new Set([
+ ...toCcAddrs,
+ ...msgCompFields.splitRecipients(newCc, true),
+ ...msgCompFields.splitRecipients(msgCompFields.bcc, true),
+ ]);
+
+ if (prevBcc) {
+ awRemoveRecipients(msgCompFields, "addr_bcc", prevBcc);
+ }
+ if (newBcc) {
+ // Add only Auto-Bcc recipients whose email is not already in To, Cc,
+ // Bcc, or added as Auto-CC from newCc declared above.
+ newBcc = msgCompFields
+ .splitRecipients(newBcc, false)
+ .filter(
+ x => !toCcBccAddrs.has(...msgCompFields.splitRecipients(x, true))
+ )
+ .join(", ");
+ awAddRecipients(msgCompFields, "addr_bcc", newBcc);
+ }
+ changedRecipients = true;
+ }
+
+ // Handle showing/hiding of empty CC/BCC row after changing identity.
+ // Whenever "Cc/Bcc these email addresses" aka mail.identity.id#.doCc/doBcc
+ // is checked in Account Settings, show the address row, even if empty.
+ // This is a feature especially for ux-efficiency of enterprise workflows.
+ let addressRowCc = document.getElementById("addressRowCc");
+ if (gCurrentIdentity.doCc) {
+ // Per identity's doCc pref, show CC row, even if empty.
+ showAndFocusAddressRow("addressRowCc");
+ } else if (
+ prevIdentity.doCc &&
+ !addressRowCc.querySelector("mail-address-pill")
+ ) {
+ // Current identity doesn't need CC row shown, but previous identity did.
+ // Hide CC row if it's empty.
+ addressRowSetVisibility(addressRowCc, false);
+ }
+
+ let addressRowBcc = document.getElementById("addressRowBcc");
+ if (gCurrentIdentity.doBcc) {
+ // Per identity's doBcc pref, show BCC row, even if empty.
+ showAndFocusAddressRow("addressRowBcc");
+ } else if (
+ prevIdentity.doBcc &&
+ !addressRowBcc.querySelector("mail-address-pill")
+ ) {
+ // Current identity doesn't need BCC row shown, but previous identity did.
+ // Hide BCC row if it's empty.
+ addressRowSetVisibility(addressRowBcc, false);
+ }
+
+ // Trigger async checking and updating of encryption UI.
+ adjustEncryptAfterIdentityChange(prevIdentity);
+
+ try {
+ gMsgCompose.identity = gCurrentIdentity;
+ } catch (ex) {
+ dump("### Cannot change the identity: " + ex + "\n");
+ }
+
+ window.dispatchEvent(new CustomEvent("compose-from-changed"));
+
+ gComposeNotificationBar.clearIdentityWarning();
+
+ // Trigger this method only if the Cc or Bcc recipients changed from the
+ // previous identity.
+ if (changedRecipients) {
+ onRecipientsChanged(true);
+ }
+ }
+
+ // Only do this if we aren't starting up...
+ // It gets done as part of startup already.
+ addRecipientsToIgnoreList(gCurrentIdentity.fullAddress);
+
+ // If the From field is editable, reset the address from the identity.
+ if (identityElement.editable) {
+ identityElement.value = identityElement.selectedItem.value;
+ identityElement.placeholder = getComposeBundle().getFormattedString(
+ "msgIdentityPlaceholder",
+ [identityElement.selectedItem.value]
+ );
+ }
+
+ editor.enableUndo(true);
+ editor.resetModificationCount();
+ selection.collapse(startNode, start);
+
+ // Try to focus the first available address row. If there are none, focus the
+ // Subject which is always available.
+ for (let row of document.querySelectorAll(".address-row")) {
+ if (focusAddressRowInput(row)) {
+ return;
+ }
+ }
+ focusSubjectInput();
+}
+
+function MakeFromFieldEditable(ignoreWarning) {
+ let bundle = getComposeBundle();
+ if (
+ !ignoreWarning &&
+ !Services.prefs.getBoolPref("mail.compose.warned_about_customize_from")
+ ) {
+ var check = { value: false };
+ if (
+ Services.prompt.confirmEx(
+ window,
+ bundle.getString("customizeFromAddressTitle"),
+ bundle.getString("customizeFromAddressWarning"),
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL +
+ Services.prompt.BUTTON_POS_1_DEFAULT,
+ null,
+ null,
+ null,
+ bundle.getString("customizeFromAddressIgnore"),
+ check
+ ) != 0
+ ) {
+ return;
+ }
+ Services.prefs.setBoolPref(
+ "mail.compose.warned_about_customize_from",
+ check.value
+ );
+ }
+
+ let customizeMenuitem = document.getElementById("cmd_customizeFromAddress");
+ customizeMenuitem.setAttribute("disabled", "true");
+ let identityElement = document.getElementById("msgIdentity");
+ let identityElementWidth = `${
+ identityElement.getBoundingClientRect().width
+ }px`;
+ identityElement.style.width = identityElementWidth;
+ identityElement.removeAttribute("type");
+ identityElement.setAttribute("editable", "true");
+ identityElement.focus();
+ identityElement.value = identityElement.selectedItem.value;
+ identityElement.select();
+ identityElement.placeholder = bundle.getFormattedString(
+ "msgIdentityPlaceholder",
+ [identityElement.selectedItem.value]
+ );
+}
+
+/**
+ * Set up autocomplete search parameters for address inputs of inbuilt headers.
+ *
+ * @param {Element} input - The address input of an inbuilt header field.
+ */
+function setupAutocompleteInput(input) {
+ let params = JSON.parse(input.getAttribute("autocompletesearchparam"));
+ params.type = input.closest(".address-row").dataset.recipienttype;
+ input.setAttribute("autocompletesearchparam", JSON.stringify(params));
+
+ // This method overrides the autocomplete binding's openPopup (essentially
+ // duplicating the logic from the autocomplete popup binding's
+ // openAutocompletePopup method), modifying it so that the popup is aligned
+ // and sized based on the parentNode of the input field.
+ input.openPopup = () => {
+ if (input.focused) {
+ input.popup.openAutocompletePopup(
+ input.nsIAutocompleteInput,
+ input.closest(".address-container")
+ );
+ }
+ };
+}
+
+/**
+ * Handle the keypress event of the From field.
+ *
+ * @param {Event} event - A DOM keypress event on #msgIdentity.
+ */
+function fromKeyPress(event) {
+ if (event.key == "Enter") {
+ // Move the focus to the first available address input.
+ document
+ .querySelector(
+ "#recipientsContainer .address-row:not(.hidden) .address-row-input"
+ )
+ .focus();
+ }
+}
+
+/**
+ * Handle the keypress event of the subject input.
+ *
+ * @param {Event} event - A DOM keypress event on #msgSubject.
+ */
+function subjectKeyPress(event) {
+ if (event.key == "Delete" && event.repeat && gPreventRowDeletionKeysRepeat) {
+ // Prevent repeated Delete keypress event if the flag is set.
+ event.preventDefault();
+ return;
+ }
+ // Enable repeated deletion if any other key is pressed, or if the Delete
+ // keypress event is not repeated, or if the flag is already false.
+ gPreventRowDeletionKeysRepeat = false;
+
+ // Move the focus to the body only if the Enter key is pressed without any
+ // modifier, as that would mean the user wants to send the message.
+ if (event.key == "Enter" && !event.ctrlKey && !event.metaKey) {
+ focusMsgBody();
+ }
+}
+
+/**
+ * Handle the input event of the subject input element.
+ *
+ * @param {Event} event - A DOM input event on #msgSubject.
+ */
+function msgSubjectOnInput(event) {
+ gSubjectChanged = true;
+ gContentChanged = true;
+ SetComposeWindowTitle();
+}
+
+// Content types supported in the envelopeDragObserver.
+const DROP_FLAVORS = [
+ "application/x-moz-file",
+ "text/x-moz-address",
+ "text/x-moz-message",
+ "text/x-moz-url",
+ "text/uri-list",
+];
+
+// We can drag and drop addresses, files, messages and urls into the compose
+// envelope.
+var envelopeDragObserver = {
+ /**
+ * Adjust the drop target when dragging from the attachment bucket onto itself
+ * by picking the nearest possible insertion point (generally, between two
+ * list items).
+ *
+ * @param {Event} event - The drag-and-drop event being performed.
+ * @returns {attachmentitem|string} - the adjusted drop target:
+ * - an attachmentitem node for inserting *before*
+ * - "none" if this isn't a valid insertion point
+ * - "afterLastItem" for appending at the bottom of the list.
+ */
+ _adjustDropTarget(event) {
+ let target = event.target;
+ if (target == gAttachmentBucket) {
+ // Dragging or dropping at top/bottom border of the listbox
+ if (
+ (event.screenY - target.screenY) /
+ target.getBoundingClientRect().height <
+ 0.5
+ ) {
+ target = gAttachmentBucket.firstElementChild;
+ } else {
+ target = gAttachmentBucket.lastElementChild;
+ }
+ // We'll check below if this is a valid target.
+ } else if (target.id == "attachmentBucketCount") {
+ // Dragging or dropping at top border of the listbox.
+ // Allow bottom half of attachment list header as extended drop target
+ // for top of list, because otherwise it would be too small.
+ if (
+ (event.screenY - target.screenY) /
+ target.getBoundingClientRect().height >=
+ 0.5
+ ) {
+ target = gAttachmentBucket.firstElementChild;
+ // We'll check below if this is a valid target.
+ } else {
+ // Top half of attachment list header: sorry, can't drop here.
+ return "none";
+ }
+ }
+
+ // Target is an attachmentitem.
+ if (target.matches("richlistitem.attachmentItem")) {
+ // If we're dragging/dropping in bottom half of attachmentitem,
+ // adjust target to target.nextElementSibling (to show dropmarker above that).
+ if (
+ (event.screenY - target.screenY) /
+ target.getBoundingClientRect().height >=
+ 0.5
+ ) {
+ target = target.nextElementSibling;
+
+ // If there's no target.nextElementSibling, we're dragging/dropping
+ // to the bottom of the list.
+ if (!target) {
+ // We can't move a bottom block selection to the bottom.
+ if (attachmentsSelectionIsBlock("bottom")) {
+ return "none";
+ }
+
+ // Not a bottom block selection: Target is *after* the last item.
+ return "afterLastItem";
+ }
+ }
+ // Check if the adjusted target attachmentitem is a valid target.
+ let isBlock = attachmentsSelectionIsBlock();
+ let prevItem = target.previousElementSibling;
+ // If target is first list item, there's no previous sibling;
+ // treat like unselected previous sibling.
+ let prevSelected = prevItem ? prevItem.selected : false;
+ if (
+ (target.selected && (isBlock || prevSelected)) ||
+ // target at end of block selection
+ (isBlock && prevSelected)
+ ) {
+ // We can't move a block selection before/after itself,
+ // or any selection onto itself, so trigger dropeffect "none".
+ return "none";
+ }
+ return target;
+ }
+
+ return "none";
+ },
+
+ _showDropMarker(targetItem) {
+ // Hide old drop marker.
+ this._hideDropMarker();
+
+ if (targetItem == "afterLastItem") {
+ targetItem = gAttachmentBucket.lastElementChild;
+ targetItem.setAttribute("dropOn", "after");
+ } else {
+ targetItem.setAttribute("dropOn", "before");
+ }
+ },
+
+ _hideDropMarker() {
+ gAttachmentBucket
+ .querySelector(".attachmentItem[dropOn]")
+ ?.removeAttribute("dropOn");
+ },
+
+ /**
+ * Loop through all the valid data type flavors and return a list of valid
+ * attachments to handle the various drag&drop actions.
+ *
+ * @param {Event} event - The drag-and-drop event being performed.
+ * @param {boolean} isDropping - If the action was performed from the onDrop
+ * method and it needs to handle pills creation.
+ *
+ * @returns {nsIMsgAttachment[]} - The array of valid attachments.
+ */
+ getValidAttachments(event, isDropping) {
+ let attachments = [];
+ let dt = event.dataTransfer;
+ let dataList = [];
+
+ // Extract all the flavors matching the data type of the dragged elements.
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ let types = Array.from(dt.mozTypesAt(i));
+ for (let flavor of DROP_FLAVORS) {
+ if (types.includes(flavor)) {
+ let data = dt.mozGetDataAt(flavor, i);
+ if (data) {
+ dataList.push({ data, flavor });
+ break;
+ }
+ }
+ }
+ }
+
+ // Check if we have any valid attachment in the dragged data.
+ for (let { data, flavor } of dataList) {
+ gIsValidInline = false;
+ let isValidAttachment = false;
+ let prettyName;
+ let size;
+ let contentType;
+ let msgUri;
+ let cloudFileInfo;
+
+ // We could be dropping an attachment of various flavors OR an address;
+ // check and do the right thing.
+ switch (flavor) {
+ // Process attachments.
+ case "application/x-moz-file":
+ if (data instanceof Ci.nsIFile) {
+ size = data.fileSize;
+ }
+ try {
+ data = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getURLSpecFromActualFile(data);
+ isValidAttachment = true;
+ } catch (e) {
+ console.error(
+ "Couldn't process the dragged file " + data.leafName + ":" + e
+ );
+ }
+ break;
+
+ case "text/x-moz-message":
+ isValidAttachment = true;
+ let msgHdr =
+ MailServices.messageServiceFromURI(data).messageURIToMsgHdr(data);
+ prettyName = msgHdr.mime2DecodedSubject;
+ if (Services.prefs.getBoolPref("mail.forward_add_extension")) {
+ prettyName += ".eml";
+ }
+
+ size = msgHdr.messageSize;
+ contentType = "message/rfc822";
+ break;
+
+ // Data type representing:
+ // - URL strings dragged from a URL bar (Allow both attach and append).
+ // NOTE: This only works for macOS and Windows.
+ // - Attachments dragged from another message (Only attach).
+ // - Images dragged from the body of another message (Only append).
+ case "text/uri-list":
+ case "text/x-moz-url":
+ let pieces = data.split("\n");
+ data = pieces[0];
+ if (pieces.length > 1) {
+ prettyName = pieces[1];
+ }
+ if (pieces.length > 2) {
+ size = parseInt(pieces[2]);
+ }
+ if (pieces.length > 3) {
+ contentType = pieces[3];
+ }
+ if (pieces.length > 4) {
+ msgUri = pieces[4];
+ }
+ if (pieces.length > 6) {
+ cloudFileInfo = {
+ cloudFileAccountKey: pieces[5],
+ cloudPartHeaderData: pieces[6],
+ };
+ }
+
+ // Show the attachment overlay only if the user is not dragging an
+ // image form another message, since we can't get the correct file
+ // name, nor we can properly handle the append inline outside the
+ // editor drop event.
+ isValidAttachment = !event.dataTransfer.types.includes(
+ "application/x-moz-nativeimage"
+ );
+ // Show the append inline overlay only if this is not a file that was
+ // dragged from the attachment bucket of another message.
+ gIsValidInline = !event.dataTransfer.types.includes(
+ "application/x-moz-file-promise"
+ );
+ break;
+
+ // Process address: Drop it into recipient field.
+ case "text/x-moz-address":
+ // Process the drop only if the message body wasn't the target and we
+ // called this method from the onDrop() method.
+ if (event.target.baseURI != "about:blank?compose" && isDropping) {
+ DropRecipient(event.target, data);
+ // Prevent the default behaviour which drops the address text into
+ // the widget.
+ event.preventDefault();
+ }
+ break;
+ }
+
+ // Create the attachment and add it to attachments array.
+ if (isValidAttachment) {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.url = data;
+ attachment.name = prettyName;
+ attachment.contentType = contentType;
+ attachment.msgUri = msgUri;
+
+ if (size !== undefined) {
+ attachment.size = size;
+ }
+
+ if (cloudFileInfo) {
+ attachment.cloudFileAccountKey = cloudFileInfo.cloudFileAccountKey;
+ attachment.cloudPartHeaderData = cloudFileInfo.cloudPartHeaderData;
+ }
+
+ attachments.push(attachment);
+ }
+ }
+
+ return attachments;
+ },
+
+ /**
+ * Reorder the attachments dragged within the attachment bucket.
+ *
+ * @param {Event} event - The drag event.
+ */
+ _reorderDraggedAttachments(event) {
+ // Adjust the drop target according to mouse position on list (items).
+ let target = this._adjustDropTarget(event);
+ // Get a non-live, sorted list of selected attachment list items.
+ let selItems = attachmentsSelectionGetSortedArray();
+ // Keep track of the item we had focused originally. Deselect it though,
+ // since listbox gets confused if you move its focused item around.
+ let focus = gAttachmentBucket.currentItem;
+ gAttachmentBucket.currentItem = null;
+ // Moving possibly non-coherent multiple selections around correctly
+ // is much more complex than one might think...
+ if (
+ (target.matches && target.matches("richlistitem.attachmentItem")) ||
+ target == "afterLastItem"
+ ) {
+ // Drop before targetItem in the list, or after last item.
+ let blockItems = [];
+ let targetItem;
+ for (let item of selItems) {
+ blockItems.push(item);
+ if (target == "afterLastItem") {
+ // Original target is the end of the list; append all items there.
+ gAttachmentBucket.appendChild(item);
+ } else if (target == selItems[0]) {
+ // Original target is first item of first selected block.
+ if (blockItems.includes(target)) {
+ // Item is in first block: do nothing, find the end of the block.
+ let nextItem = item.nextElementSibling;
+ if (!nextItem || !nextItem.selected) {
+ // We've reached the end of the first block.
+ blockItems.length = 0;
+ targetItem = nextItem;
+ }
+ } else {
+ // Item is NOT in first block: insert before targetItem,
+ // i.e. after end of first block.
+ gAttachmentBucket.insertBefore(item, targetItem);
+ }
+ } else if (target.selected) {
+ // Original target is not first item of first block,
+ // but first item of another block.
+ if (
+ gAttachmentBucket.getIndexOfItem(item) <
+ gAttachmentBucket.getIndexOfItem(target)
+ ) {
+ // Insert all items from preceding blocks before original target.
+ gAttachmentBucket.insertBefore(item, target);
+ } else if (blockItems.includes(target)) {
+ // target is included in any selected block except first:
+ // do nothing for that block, find its end.
+ let nextItem = item.nextElementSibling;
+ if (!nextItem || !nextItem.selected) {
+ // end of block containing target
+ blockItems.length = 0;
+ targetItem = nextItem;
+ }
+ } else {
+ // Item from block after block containing target: insert before
+ // targetItem, i.e. after end of block containing target.
+ gAttachmentBucket.insertBefore(item, targetItem);
+ }
+ } else {
+ // target != selItems [0]
+ // Original target is NOT first item of any block, and NOT selected:
+ // Insert all items before the original target.
+ gAttachmentBucket.insertBefore(item, target);
+ }
+ }
+ }
+ gAttachmentBucket.currentItem = focus;
+ },
+
+ handleInlineDrop(event) {
+ // It would be nice here to be able to append images, but we can't really
+ // assume if users want to add the image URL as clickable link or embedded
+ // image, so we always default to clickable link.
+ // We can later explore adding some UI choice to allow controlling the
+ // outcome of this drop action, but users can still copy and paste the image
+ // in the editor to cirumvent this potential issue.
+ let editor = GetCurrentEditor();
+ let attachments = this.getValidAttachments(event, true);
+
+ for (let attachment of attachments) {
+ if (!attachment?.url) {
+ continue;
+ }
+
+ let link = editor.createElementWithDefaults("a");
+ link.setAttribute("href", attachment.url);
+ link.textContent =
+ attachment.name ||
+ gMsgCompose.AttachmentPrettyName(attachment.url, null);
+ editor.insertElementAtSelection(link, true);
+ }
+ },
+
+ async onDrop(event) {
+ this._hideDropOverlay();
+
+ let dragSession = gDragService.getCurrentSession();
+ if (dragSession.sourceNode?.parentNode == gAttachmentBucket) {
+ // We dragged from the attachment pane onto itself, so instead of
+ // attaching a new object, we're just reordering them.
+ this._reorderDraggedAttachments(event);
+ this._hideDropMarker();
+ return;
+ }
+
+ // Interrupt if we're dropping elements from within the message body.
+ if (dragSession.sourceNode?.ownerDocument.URL == "about:blank?compose") {
+ return;
+ }
+
+ // Interrupt if we're not dropping a file from outside the compose window
+ // and we're not dragging a supported data type.
+ if (
+ !event.dataTransfer.files.length &&
+ !DROP_FLAVORS.some(f => event.dataTransfer.types.includes(f))
+ ) {
+ return;
+ }
+
+ // If the drop happened on the inline container, and the dragged data is
+ // valid for inline, bail out and handle it as inline text link.
+ if (event.target.id == "addInline" && gIsValidInline) {
+ this.handleInlineDrop(event);
+ return;
+ }
+
+ // Handle the inline adding of images without triggering the creation of
+ // any attachment if the user dropped only images above the #addInline box.
+ if (
+ event.target.id == "addInline" &&
+ !this.isNotDraggingOnlyImages(event.dataTransfer)
+ ) {
+ this.appendImagesInline(event.dataTransfer);
+ return;
+ }
+
+ let attachments = this.getValidAttachments(event, true);
+
+ // Interrupt if we don't have anything to attach.
+ if (!attachments.length) {
+ return;
+ }
+
+ let addedAttachmentItems = await AddAttachments(attachments);
+ // Convert attachments back to cloudFiles, if any.
+ for (let attachmentItem of addedAttachmentItems) {
+ if (
+ !attachmentItem.attachment.cloudFileAccountKey ||
+ !attachmentItem.attachment.cloudPartHeaderData
+ ) {
+ continue;
+ }
+ try {
+ let account = cloudFileAccounts.getAccount(
+ attachmentItem.attachment.cloudFileAccountKey
+ );
+ let upload = JSON.parse(
+ atob(attachmentItem.attachment.cloudPartHeaderData)
+ );
+ await UpdateAttachment(attachmentItem, {
+ cloudFileAccount: account,
+ relatedCloudFileUpload: upload,
+ });
+ } catch (ex) {
+ showLocalizedCloudFileAlert(ex);
+ }
+ }
+ gAttachmentBucket.focus();
+
+ // Stop the propagation only if we actually attached something.
+ event.stopPropagation();
+ },
+
+ onDragOver(event) {
+ let dragSession = gDragService.getCurrentSession();
+
+ // Check if we're dragging from the attachment bucket onto itself.
+ if (dragSession.sourceNode?.parentNode == gAttachmentBucket) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // Show a drop marker.
+ let target = this._adjustDropTarget(event);
+
+ if (
+ (target.matches && target.matches("richlistitem.attachmentItem")) ||
+ target == "afterLastItem"
+ ) {
+ // Adjusted target is an attachment list item; show dropmarker.
+ this._showDropMarker(target);
+ return;
+ }
+
+ // target == "none", target is not a listItem, or no target:
+ // Indicate that we can't drop here.
+ this._hideDropMarker();
+ event.dataTransfer.dropEffect = "none";
+ return;
+ }
+
+ // Interrupt if we're dragging elements from within the message body.
+ if (dragSession.sourceNode?.ownerDocument.URL == "about:blank?compose") {
+ return;
+ }
+
+ // No need to check for the same dragged files if the previous dragging
+ // action didn't end.
+ if (gIsDraggingAttachments) {
+ // Prevent the default action of the event otherwise the onDrop event
+ // won't be triggered.
+ event.preventDefault();
+ this.detectHoveredOverlay(event.target.id);
+ return;
+ }
+
+ if (DROP_FLAVORS.some(f => event.dataTransfer.types.includes(f))) {
+ // Show the drop overlay only if we dragged files or supported types.
+ let attachments = this.getValidAttachments(event);
+ if (attachments.length) {
+ // We're dragging files that can potentially be attached or added
+ // inline, so update the variable.
+ gIsDraggingAttachments = true;
+
+ event.stopPropagation();
+ event.preventDefault();
+ document
+ .getElementById("dropAttachmentOverlay")
+ .classList.add("showing");
+
+ document.l10n.setAttributes(
+ document.getElementById("addAsAttachmentLabel"),
+ "drop-file-label-attachment",
+ {
+ count: attachments.length || 1,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("addInlineLabel"),
+ "drop-file-label-inline",
+ {
+ count: attachments.length || 1,
+ }
+ );
+
+ // Show the #addInline box only if the user is dragging text that we
+ // want to allow adding as text, as well as dragging only images, and
+ // if this is not a plain text message.
+ // NOTE: We're using event.dataTransfer.files.length instead of
+ // attachments.length because we only need to consider images coming
+ // from outside the application. The attachments array might contain
+ // files dragged from other compose windows or received message, which
+ // should not trigger the inline attachment overlay.
+ document
+ .getElementById("addInline")
+ .classList.toggle(
+ "hidden",
+ !gIsValidInline &&
+ (!event.dataTransfer.files.length ||
+ this.isNotDraggingOnlyImages(event.dataTransfer) ||
+ !gMsgCompose.composeHTML)
+ );
+ } else {
+ DragAddressOverTargetControl(event);
+ }
+ }
+
+ this.detectHoveredOverlay(event.target.id);
+ },
+
+ onDragLeave(event) {
+ // Set the variable to false as a drag leave event was triggered.
+ gIsDraggingAttachments = false;
+
+ // We use a timeout since a drag leave event might occur also when the drag
+ // motion passes above a child element and doesn't actually leave the
+ // compose window.
+ setTimeout(() => {
+ // If after the timeout, the dragging boolean is true, it means the user
+ // is still dragging something above the compose window, so let's bail out
+ // to prevent visual flickering of the drop overlay.
+ if (gIsDraggingAttachments) {
+ return;
+ }
+
+ this._hideDropOverlay();
+ }, 100);
+
+ this._hideDropMarker();
+ },
+
+ /**
+ * Hide the drag & drop overlay and update the global dragging variable to
+ * false. This operations are set in a dedicated method since they need to be
+ * called outside of the onDragleave() method.
+ */
+ _hideDropOverlay() {
+ gIsDraggingAttachments = false;
+
+ let overlay = document.getElementById("dropAttachmentOverlay");
+ overlay.classList.remove("showing");
+ overlay.classList.add("hiding");
+ },
+
+ /**
+ * Loop through all the currently dragged or dropped files to see if there's
+ * at least 1 file which is not an image.
+ *
+ * @param {DataTransfer} dataTransfer - The dataTransfer object from the drag
+ * or drop event.
+ * @returns {boolean} True if at least one file is not an image.
+ */
+ isNotDraggingOnlyImages(dataTransfer) {
+ for (let file of dataTransfer.files) {
+ if (!file.type.includes("image/")) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Add or remove the hover effect to the droppable containers. We can't do it
+ * simply via CSS since the hover events don't work when dragging an item.
+ *
+ * @param {string} targetId - The ID of the hovered overlay element.
+ */
+ detectHoveredOverlay(targetId) {
+ document
+ .getElementById("addInline")
+ .classList.toggle("hover", targetId == "addInline");
+ document
+ .getElementById("addAsAttachment")
+ .classList.toggle("hover", targetId == "addAsAttachment");
+ },
+
+ /**
+ * Loop through all the images that have been dropped above the #addInline
+ * box and create an image element to append to the message body.
+ *
+ * @param {DataTransfer} dataTransfer - The dataTransfer object from the drop
+ * event.
+ */
+ appendImagesInline(dataTransfer) {
+ focusMsgBody();
+ let editor = GetCurrentEditor();
+ editor.beginTransaction();
+
+ for (let file of dataTransfer.files) {
+ if (!file.mozFullPath) {
+ continue;
+ }
+
+ let realFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ realFile.initWithPath(file.mozFullPath);
+
+ let imageElement;
+ try {
+ imageElement = editor.createElementWithDefaults("img");
+ } catch (e) {
+ dump("Failed to create a new image element!\n");
+ console.error(e);
+ continue;
+ }
+
+ let src = Services.io.newFileURI(realFile).spec;
+ imageElement.setAttribute("src", src);
+ imageElement.setAttribute("moz-do-not-send", "false");
+
+ editor.insertElementAtSelection(imageElement, true);
+
+ try {
+ loadBlockedImage(src);
+ } catch (e) {
+ dump("Failed to load the appended image!\n");
+ console.error(e);
+ continue;
+ }
+ }
+
+ editor.endTransaction();
+ },
+};
+
+// See attachmentListDNDObserver, which should have the same logic.
+let attachmentBucketDNDObserver = {
+ onDragStart(event) {
+ // NOTE: Starting a drag on an attachment item will normally also select
+ // the attachment item before this method is called. But this is not
+ // necessarily the case. E.g. holding Shift when starting the drag
+ // operation. When it isn't selected, we just don't transfer.
+ if (event.target.matches(".attachmentItem[selected]")) {
+ // Also transfer other selected attachment items.
+ let attachments = Array.from(
+ gAttachmentBucket.querySelectorAll(".attachmentItem[selected]"),
+ item => item.attachment
+ );
+ setupDataTransfer(event, attachments);
+ }
+ event.stopPropagation();
+ },
+};
+
+function DisplaySaveFolderDlg(folderURI) {
+ try {
+ var showDialog = gCurrentIdentity.showSaveMsgDlg;
+ } catch (e) {
+ return;
+ }
+
+ if (showDialog) {
+ let msgfolder = MailUtils.getExistingFolder(folderURI);
+ if (!msgfolder) {
+ return;
+ }
+ let checkbox = { value: 0 };
+ let bundle = getComposeBundle();
+ let SaveDlgTitle = bundle.getString("SaveDialogTitle");
+ let dlgMsg = bundle.getFormattedString("SaveDialogMsg", [
+ msgfolder.name,
+ msgfolder.server.prettyName,
+ ]);
+
+ Services.prompt.alertCheck(
+ window,
+ SaveDlgTitle,
+ dlgMsg,
+ bundle.getString("CheckMsg"),
+ checkbox
+ );
+ try {
+ gCurrentIdentity.showSaveMsgDlg = !checkbox.value;
+ } catch (e) {}
+ }
+}
+
+/**
+ * Focus the people search input in the contacts side panel.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @returns {boolean} - Whether the peopleSearchInput was focused.
+ */
+function focusContactsSidebarSearchInput() {
+ if (document.getElementById("contactsSplitter").isCollapsed) {
+ return false;
+ }
+ let input = document
+ .getElementById("contactsBrowser")
+ .contentDocument.getElementById("peopleSearchInput");
+ if (!input) {
+ return false;
+ }
+ input.focus();
+ return true;
+}
+
+/**
+ * Focus the "From" identity input/selector.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @returns {true} - Always returns true.
+ */
+function focusMsgIdentity() {
+ document.getElementById("msgIdentity").focus();
+ return true;
+}
+
+/**
+ * Focus the address row input, provided the row is not hidden.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @param {Element} row - The address row to focus.
+ *
+ * @returns {boolean} - Whether the input was focused.
+ */
+function focusAddressRowInput(row) {
+ if (row.classList.contains("hidden")) {
+ return false;
+ }
+ row.querySelector(".address-row-input").focus();
+ return true;
+}
+
+/**
+ * Focus the "Subject" input.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @returns {true} - Always returns true.
+ */
+function focusSubjectInput() {
+ document.getElementById("msgSubject").focus();
+ return true;
+}
+
+/**
+ * Focus the composed message body.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @returns {true} - Always returns true.
+ */
+function focusMsgBody() {
+ // window.content.focus() fails to blur the currently focused element
+ document.commandDispatcher.advanceFocusIntoSubtree(
+ document.getElementById("messageArea")
+ );
+ return true;
+}
+
+/**
+ * Focus the attachment bucket, provided it is not hidden.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @param {Element} attachmentArea - The attachment container.
+ *
+ * @returns {boolean} - Whether the attachment bucket was focused.
+ */
+function focusAttachmentBucket(attachmentArea) {
+ if (
+ document
+ .getElementById("composeContentBox")
+ .classList.contains("attachment-area-hidden")
+ ) {
+ return false;
+ }
+ if (!attachmentArea.open) {
+ // Focus the expander instead.
+ attachmentArea.querySelector("summary").focus();
+ return true;
+ }
+ gAttachmentBucket.focus();
+ return true;
+}
+
+/**
+ * Focus the first notification button.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @returns {boolean} - Whether a notification received focused.
+ */
+function focusNotification() {
+ let notification = gComposeNotification.allNotifications[0];
+ if (notification) {
+ let button = notification.buttonContainer.querySelector("button");
+ if (button) {
+ button.focus();
+ } else {
+ // Focus the close button instead.
+ notification.closeButton.focus();
+ }
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Focus the first focusable descendant of the status bar.
+ *
+ * Note, this is used as a {@link moveFocusWithin} method.
+ *
+ * @param {Element} attachmentArea - The status bar.
+ *
+ * @returns {boolean} - Whether a status bar descendant received focused.
+ */
+function focusStatusBar(statusBar) {
+ let button = statusBar.querySelector("button:not([hidden])");
+ if (!button) {
+ return false;
+ }
+ button.focus();
+ return true;
+}
+
+/**
+ * Fast-track focus ring: Switch focus between important (not all) elements
+ * in the message compose window in response to Ctrl+[Shift+]Tab or [Shift+]F6.
+ *
+ * @param {Event} event - A DOM keyboard event of a fast focus ring shortcut key
+ */
+function moveFocusToNeighbouringArea(event) {
+ event.preventDefault();
+ let currentElement = document.activeElement;
+
+ for (let i = 0; i < gFocusAreas.length; i++) {
+ // Go through each area and check if focus is within.
+ let area = gFocusAreas[i];
+ if (!area.root.contains(currentElement)) {
+ continue;
+ }
+ // Focus is within, so we find the neighbouring area to move focus to.
+ let end = i;
+ while (true) {
+ // Get the next neighbour.
+ // NOTE: The focus will loop around.
+ if (event.shiftKey) {
+ // Move focus backward. If the index points to the start of the Array,
+ // we loop back to the end of the Array.
+ i = (i || gFocusAreas.length) - 1;
+ } else {
+ // Move focus forward. If the index points to the end of the Array, we
+ // loop back to the start of the Array.
+ i = (i + 1) % gFocusAreas.length;
+ }
+ if (i == end) {
+ // Full loop around without finding an area to focus.
+ // Unexpected, but we make sure to stop looping.
+ break;
+ }
+ area = gFocusAreas[i];
+ if (area.focus(area.root)) {
+ // Successfully moved focus.
+ break;
+ }
+ // Else, try the next neighbour.
+ }
+ return;
+ }
+ // Focus is currently outside the gFocusAreas list, so do nothing.
+}
+
+/**
+ * If the contacts sidebar is shown, hide it. Otherwise, show the contacts
+ * sidebar and focus it.
+ */
+function toggleContactsSidebar() {
+ setContactsSidebarVisibility(
+ document.getElementById("contactsSplitter").isCollapsed,
+ true
+ );
+}
+
+/**
+ * Show or hide contacts sidebar.
+ *
+ * @param {boolean} show - Whether to show the sidebar or hide the sidebar.
+ * @param {boolean} focus - Whether to focus peopleSearchInput if the sidebar is
+ * shown.
+ */
+function setContactsSidebarVisibility(show, focus) {
+ let contactsSplitter = document.getElementById("contactsSplitter");
+ let sidebarAddrMenu = document.getElementById("menu_AddressSidebar");
+ let contactsButton = document.getElementById("button-contacts");
+
+ if (show) {
+ contactsSplitter.expand();
+ sidebarAddrMenu.setAttribute("checked", "true");
+ if (contactsButton) {
+ contactsButton.setAttribute("checked", "true");
+ }
+
+ let contactsBrowser = document.getElementById("contactsBrowser");
+ if (contactsBrowser.getAttribute("src") == "") {
+ // Url not yet set, load contacts side bar and focus the search
+ // input if applicable: We pass "?focus" as a URL querystring, then via
+ // onload event of <window id="abContactsPanel">, in AbPanelLoad() of
+ // abContactsPanel.js, we do the focusing first thing to avoid timing
+ // issues when trying to focus from here while contacts side bar is still
+ // loading.
+ let url = "chrome://messenger/content/addressbook/abContactsPanel.xhtml";
+ if (focus) {
+ url += "?focus";
+ }
+ contactsBrowser.setAttribute("src", url);
+ } else if (focus) {
+ // Url already set, so we can focus immediately if applicable.
+ focusContactsSidebarSearchInput();
+ }
+ } else {
+ let contactsSidebar = document.getElementById("contactsSidebar");
+ // Before closing, check if the focus was within the contacts sidebar.
+ let sidebarFocussed = contactsSidebar.contains(document.activeElement);
+
+ contactsSplitter.collapse();
+ sidebarAddrMenu.removeAttribute("checked");
+ if (contactsButton) {
+ contactsButton.removeAttribute("checked");
+ }
+
+ // Don't change the focus unless it was within the contacts sidebar.
+ if (!sidebarFocussed) {
+ return;
+ }
+ // Else, we need to explicitly move the focus out of the contacts sidebar.
+ // We choose the subject input if it is empty, otherwise the message body.
+ if (!document.getElementById("msgSubject").value) {
+ focusSubjectInput();
+ } else {
+ focusMsgBody();
+ }
+ }
+}
+
+function loadHTMLMsgPrefs() {
+ let fontFace = Services.prefs.getStringPref("msgcompose.font_face", "");
+ if (fontFace) {
+ doStatefulCommand("cmd_fontFace", fontFace, true);
+ }
+
+ let fontSize = Services.prefs.getCharPref("msgcompose.font_size", "3");
+ EditorSetFontSize(fontSize);
+
+ let bodyElement = GetBodyElement();
+
+ let useDefault = Services.prefs.getBoolPref("msgcompose.default_colors");
+
+ let textColor = useDefault
+ ? ""
+ : Services.prefs.getCharPref("msgcompose.text_color", "");
+ if (!bodyElement.getAttribute("text") && textColor) {
+ bodyElement.setAttribute("text", textColor);
+ gDefaultTextColor = textColor;
+ document.getElementById("cmd_fontColor").setAttribute("state", textColor);
+ onFontColorChange();
+ }
+
+ let bgColor = useDefault
+ ? ""
+ : Services.prefs.getCharPref("msgcompose.background_color", "");
+ if (!bodyElement.getAttribute("bgcolor") && bgColor) {
+ bodyElement.setAttribute("bgcolor", bgColor);
+ gDefaultBackgroundColor = bgColor;
+ document
+ .getElementById("cmd_backgroundColor")
+ .setAttribute("state", bgColor);
+ onBackgroundColorChange();
+ }
+}
+
+async function AutoSave() {
+ if (
+ gMsgCompose.editor &&
+ (gContentChanged || gMsgCompose.bodyModified) &&
+ !gSendOperationInProgress &&
+ !gSaveOperationInProgress
+ ) {
+ try {
+ await GenericSendMessage(Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft);
+ } catch (ex) {
+ console.error(ex);
+ }
+ gAutoSaveKickedIn = true;
+ }
+
+ gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval);
+}
+
+/**
+ * Periodically check for keywords in the message.
+ */
+var gAttachmentNotifier = {
+ _obs: null,
+
+ enabled: false,
+
+ init(aDocument) {
+ if (this._obs) {
+ this.shutdown();
+ }
+
+ this.enabled = Services.prefs.getBoolPref(
+ "mail.compose.attachment_reminder"
+ );
+ if (!this.enabled) {
+ return;
+ }
+
+ this._obs = new MutationObserver(function (aMutations) {
+ gAttachmentNotifier.timer.cancel();
+ gAttachmentNotifier.timer.initWithCallback(
+ gAttachmentNotifier.event,
+ 500,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ });
+
+ this._obs.observe(aDocument, {
+ attributes: true,
+ childList: true,
+ characterData: true,
+ subtree: true,
+ });
+
+ // Add an input event listener for the subject field since there
+ // are ways of changing its value without key presses.
+ document
+ .getElementById("msgSubject")
+ .addEventListener("input", this.subjectInputObserver, true);
+
+ // We could have been opened with a draft message already containing
+ // some keywords, so run the checker once to pick them up.
+ this.event.notify();
+ },
+
+ // Timer based function triggered by the inputEventListener
+ // for the subject field.
+ subjectInputObserver() {
+ gAttachmentNotifier.timer.cancel();
+ gAttachmentNotifier.timer.initWithCallback(
+ gAttachmentNotifier.event,
+ 500,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ /**
+ * Checks for new keywords synchronously and run the usual handler.
+ *
+ * @param aManage Determines whether to manage the notification according to keywords found.
+ */
+ redetectKeywords(aManage) {
+ if (!this.enabled) {
+ return;
+ }
+
+ attachmentWorker.onmessage(
+ { data: this._checkForAttachmentKeywords(false) },
+ aManage
+ );
+ },
+
+ /**
+ * Check if there are any keywords in the message.
+ *
+ * @param async Whether we should run the regex checker asynchronously or not.
+ *
+ * @returns If async is true, attachmentWorker.message is called with the array
+ * of found keywords and this function returns null.
+ * If it is false, the array is returned from this function immediately.
+ */
+ _checkForAttachmentKeywords(async) {
+ if (!this.enabled) {
+ return async ? null : [];
+ }
+
+ if (attachmentNotificationSupressed()) {
+ // If we know we don't need to show the notification,
+ // we can skip the expensive checking of keywords in the message.
+ // but mark it in the .lastMessage that the keywords are unknown.
+ attachmentWorker.lastMessage = null;
+ return async ? null : [];
+ }
+
+ let keywordsInCsv = Services.prefs.getComplexValue(
+ "mail.compose.attachment_reminder_keywords",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ let mailBody = getBrowser().contentDocument.querySelector("body");
+
+ // We use a new document and import the body into it. We do that to avoid
+ // loading images that were previously blocked. Content policy of the newly
+ // created data document will block the loads. Details: Bug 1409458 comment #22.
+ let newDoc = getBrowser().contentDocument.implementation.createDocument(
+ "",
+ "",
+ null
+ );
+ let mailBodyNode = newDoc.importNode(mailBody, true);
+
+ // Don't check quoted text from reply.
+ let blockquotes = mailBodyNode.getElementsByTagName("blockquote");
+ for (let i = blockquotes.length - 1; i >= 0; i--) {
+ blockquotes[i].remove();
+ }
+
+ // For plaintext composition the quotes we need to find and exclude are
+ // <span _moz_quote="true">.
+ let spans = mailBodyNode.querySelectorAll("span[_moz_quote]");
+ for (let i = spans.length - 1; i >= 0; i--) {
+ spans[i].remove();
+ }
+
+ // Ignore signature (html compose mode).
+ let sigs = mailBodyNode.getElementsByClassName("moz-signature");
+ for (let i = sigs.length - 1; i >= 0; i--) {
+ sigs[i].remove();
+ }
+
+ // Replace brs with line breaks so node.textContent won't pull foo<br>bar
+ // together to foobar.
+ let brs = mailBodyNode.getElementsByTagName("br");
+ for (let i = brs.length - 1; i >= 0; i--) {
+ brs[i].parentNode.replaceChild(
+ mailBodyNode.ownerDocument.createTextNode("\n"),
+ brs[i]
+ );
+ }
+
+ // Ignore signature (plain text compose mode).
+ let mailData = mailBodyNode.textContent;
+ let sigIndex = mailData.indexOf("-- \n");
+ if (sigIndex > 0) {
+ mailData = mailData.substring(0, sigIndex);
+ }
+
+ // Ignore replied messages (plain text and html compose mode).
+ let repText = getComposeBundle().getString(
+ "mailnews.reply_header_originalmessage"
+ );
+ let repIndex = mailData.indexOf(repText);
+ if (repIndex > 0) {
+ mailData = mailData.substring(0, repIndex);
+ }
+
+ // Ignore forwarded messages (plain text and html compose mode).
+ let fwdText = getComposeBundle().getString(
+ "mailnews.forward_header_originalmessage"
+ );
+ let fwdIndex = mailData.indexOf(fwdText);
+ if (fwdIndex > 0) {
+ mailData = mailData.substring(0, fwdIndex);
+ }
+
+ // Prepend the subject to see if the subject contains any attachment
+ // keywords too, after making sure that the subject has changed
+ // or after reopening a draft. For reply, redirect and forward,
+ // only check when the input was changed by the user.
+ let subject = document.getElementById("msgSubject").value;
+ if (
+ subject &&
+ (gSubjectChanged ||
+ (gEditingDraft &&
+ (gComposeType == Ci.nsIMsgCompType.New ||
+ gComposeType == Ci.nsIMsgCompType.NewsPost ||
+ gComposeType == Ci.nsIMsgCompType.Draft ||
+ gComposeType == Ci.nsIMsgCompType.Template ||
+ gComposeType == Ci.nsIMsgCompType.EditTemplate ||
+ gComposeType == Ci.nsIMsgCompType.EditAsNew ||
+ gComposeType == Ci.nsIMsgCompType.MailToUrl)))
+ ) {
+ mailData = subject + " " + mailData;
+ }
+
+ if (!async) {
+ return AttachmentChecker.getAttachmentKeywords(mailData, keywordsInCsv);
+ }
+
+ attachmentWorker.postMessage([mailData, keywordsInCsv]);
+ return null;
+ },
+
+ shutdown() {
+ if (this._obs) {
+ this._obs.disconnect();
+ }
+ gAttachmentNotifier.timer.cancel();
+
+ this._obs = null;
+ },
+
+ event: {
+ notify(timer) {
+ // Only run the checker if the compose window is initialized
+ // and not shutting down.
+ if (gMsgCompose) {
+ // This runs the attachmentWorker asynchronously so if keywords are found
+ // manageAttachmentNotification is run from attachmentWorker.onmessage.
+ gAttachmentNotifier._checkForAttachmentKeywords(true);
+ }
+ },
+ },
+
+ timer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
+};
+
+/**
+ * Helper function to remove a query part from a URL, so for example:
+ * ...?remove=xx&other=yy becomes ...?other=yy.
+ *
+ * @param aURL the URL from which to remove the query part
+ * @param aQuery the query part to remove
+ * @returns the URL with the query part removed
+ */
+function removeQueryPart(aURL, aQuery) {
+ // Quick pre-check.
+ if (!aURL.includes(aQuery)) {
+ return aURL;
+ }
+
+ let indexQM = aURL.indexOf("?");
+ if (indexQM < 0) {
+ return aURL;
+ }
+
+ let queryParts = aURL.substr(indexQM + 1).split("&");
+ let indexPart = queryParts.indexOf(aQuery);
+ if (indexPart < 0) {
+ return aURL;
+ }
+ queryParts.splice(indexPart, 1);
+ return aURL.substr(0, indexQM + 1) + queryParts.join("&");
+}
+
+function InitEditor() {
+ var editor = GetCurrentEditor();
+
+ // Set eEditorMailMask flag to avoid using content prefs for spell checker,
+ // otherwise dictionary setting in preferences is ignored and dictionary is
+ // inconsistent in subject and message body.
+ let eEditorMailMask = Ci.nsIEditor.eEditorMailMask;
+ editor.flags |= eEditorMailMask;
+ document.getElementById("msgSubject").editor.flags |= eEditorMailMask;
+
+ // Control insertion of line breaks.
+ editor.returnInParagraphCreatesNewParagraph = Services.prefs.getBoolPref(
+ "editor.CR_creates_new_p"
+ );
+ editor.document.execCommand(
+ "defaultparagraphseparator",
+ false,
+ gMsgCompose.composeHTML &&
+ Services.prefs.getBoolPref("mail.compose.default_to_paragraph")
+ ? "p"
+ : "br"
+ );
+ if (gMsgCompose.composeHTML) {
+ // Re-enable table/image resizers.
+ editor.QueryInterface(
+ Ci.nsIHTMLAbsPosEditor
+ ).absolutePositioningEnabled = true;
+ editor.QueryInterface(
+ Ci.nsIHTMLInlineTableEditor
+ ).inlineTableEditingEnabled = true;
+ editor.QueryInterface(Ci.nsIHTMLObjectResizer).objectResizingEnabled = true;
+ }
+
+ // We use loadSheetUsingURIString so that we get a synchronous load, rather
+ // than having a late-finishing async load mark our editor as modified when
+ // the user hasn't typed anything yet, but that means the sheet must not
+ // @import slow things, especially not over the network.
+ let domWindowUtils = GetCurrentEditorElement().contentWindow.windowUtils;
+ domWindowUtils.loadSheetUsingURIString(
+ "chrome://messenger/skin/messageQuotes.css",
+ domWindowUtils.AGENT_SHEET
+ );
+ domWindowUtils.loadSheetUsingURIString(
+ "chrome://messenger/skin/shared/composerOverlay.css",
+ domWindowUtils.AGENT_SHEET
+ );
+
+ window.content.browsingContext.allowJavascript = false;
+ window.content.browsingContext.docShell.allowAuth = false;
+ window.content.browsingContext.docShell.allowMetaRedirects = false;
+ gMsgCompose.initEditor(editor, window.content);
+
+ if (!editor.document.doctype) {
+ editor.document.insertBefore(
+ editor.document.implementation.createDocumentType("html", "", ""),
+ editor.document.firstChild
+ );
+ }
+
+ // Then, we enable related UI entries.
+ enableInlineSpellCheck(Services.prefs.getBoolPref("mail.spellcheck.inline"));
+ gAttachmentNotifier.init(editor.document);
+
+ // Listen for spellchecker changes, set document language to
+ // dictionary picked by the user via the right-click menu in the editor.
+ document.addEventListener("spellcheck-changed", updateDocumentLanguage);
+
+ // XXX: the error event fires twice for each load. Why??
+ editor.document.body.addEventListener(
+ "error",
+ function (event) {
+ if (event.target.localName != "img") {
+ return;
+ }
+
+ if (event.target.getAttribute("moz-do-not-send") == "true") {
+ return;
+ }
+
+ let src = event.target.src;
+ if (!src) {
+ return;
+ }
+ if (!/^file:/i.test(src)) {
+ // Check if this is a protocol that can fetch parts.
+ let protocol = src.substr(0, src.indexOf(":")).toLowerCase();
+ if (
+ !(
+ Services.io.getProtocolHandler(protocol) instanceof
+ Ci.nsIMsgMessageFetchPartService
+ )
+ ) {
+ // Can't fetch parts, don't try to load.
+ return;
+ }
+ }
+
+ if (event.target.classList.contains("loading-internal")) {
+ // We're already loading this, or tried so unsuccessfully.
+ return;
+ }
+ if (gOriginalMsgURI) {
+ let msgSvc = MailServices.messageServiceFromURI(gOriginalMsgURI);
+ let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI);
+ if (
+ src.startsWith(
+ removeQueryPart(
+ originalMsgNeckoURI.spec,
+ "type=application/x-message-display"
+ )
+ ) ||
+ // Special hack for saved messages.
+ (src.includes("?number=0&") &&
+ originalMsgNeckoURI.spec.startsWith("file://") &&
+ src.startsWith(
+ removeQueryPart(
+ originalMsgNeckoURI.spec,
+ "type=application/x-message-display"
+ ).replace("file://", "mailbox://") + "number=0"
+ ))
+ ) {
+ // Reply/Forward/Edit Draft/Edit as New can contain references to
+ // images in the original message. Load those and make them data: URLs
+ // now.
+ event.target.classList.add("loading-internal");
+ try {
+ loadBlockedImage(src);
+ } catch (e) {
+ // Couldn't load the referenced image.
+ console.error(e);
+ }
+ } else {
+ // Appears to reference a random message. Notify and keep blocking.
+ gComposeNotificationBar.setBlockedContent(src);
+ }
+ } else {
+ // For file:, and references to parts of random messages, show the
+ // blocked content notification.
+ gComposeNotificationBar.setBlockedContent(src);
+ }
+ },
+ true
+ );
+
+ // Convert mailnews URL back to data: URL.
+ let background = editor.document.body.background;
+ if (background && gOriginalMsgURI) {
+ // Check that background has the same URL as the message itself.
+ let msgSvc = MailServices.messageServiceFromURI(gOriginalMsgURI);
+ let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI);
+ if (
+ background.startsWith(
+ removeQueryPart(
+ originalMsgNeckoURI.spec,
+ "type=application/x-message-display"
+ )
+ )
+ ) {
+ try {
+ editor.document.body.background = loadBlockedImage(background, true);
+ } catch (e) {
+ // Couldn't load the referenced image.
+ console.error(e);
+ }
+ }
+ }
+
+ // Run menubar initialization first, to avoid TabsInTitlebar code picking
+ // up mutations from it and causing a reflow.
+ if (AppConstants.platform != "macosx") {
+ AutoHideMenubar.init();
+ }
+
+ // For plain text compose, set the styles for quoted text according to
+ // preferences.
+ if (!gMsgCompose.composeHTML) {
+ let style = editor.document.createElement("style");
+ editor.document.head.appendChild(style);
+ let fontStyle = "";
+ let fontSize = "";
+ switch (Services.prefs.getIntPref("mail.quoted_style")) {
+ case 1:
+ fontStyle = "font-weight: bold;";
+ break;
+ case 2:
+ fontStyle = "font-style: italic;";
+ break;
+ case 3:
+ fontStyle = "font-weight: bold; font-style: italic;";
+ break;
+ }
+
+ switch (Services.prefs.getIntPref("mail.quoted_size")) {
+ case 1:
+ fontSize = "font-size: large;";
+ break;
+ case 2:
+ fontSize = "font-size: small;";
+ break;
+ }
+
+ let citationColor =
+ "color: " + Services.prefs.getCharPref("mail.citation_color") + ";";
+
+ style.sheet.insertRule(
+ `span[_moz_quote="true"] {
+ ${fontStyle}
+ ${fontSize}
+ ${citationColor}
+ }`
+ );
+ gMsgCompose.bodyModified = false;
+ }
+
+ // Set document language to the draft language or the preference
+ // if this is a draft or template we prepared.
+ let draftLanguages = null;
+ if (
+ gMsgCompose.compFields.creatorIdentityKey &&
+ gMsgCompose.compFields.contentLanguage
+ ) {
+ draftLanguages = gMsgCompose.compFields.contentLanguage
+ .split(",")
+ .map(lang => lang.trim());
+ }
+
+ let dictionaries = getValidSpellcheckerDictionaries(draftLanguages);
+ ComposeChangeLanguage(dictionaries).catch(console.error);
+}
+
+function setFontSize(event) {
+ // Increase Font Menuitem and Decrease Font Menuitem from the main menu
+ // will call this function because of oncommand attribute on the menupopup
+ // and fontSize will be null for such function calls.
+ let fontSize = event.target.value;
+ if (fontSize) {
+ EditorSetFontSize(fontSize);
+ }
+}
+
+function setParagraphState(event) {
+ editorSetParagraphState(event.target.value);
+}
+
+// This is used as event listener to spellcheck-changed event to update
+// document language.
+function updateDocumentLanguage(e) {
+ ComposeChangeLanguage(e.detail.dictionaries).catch(console.error);
+}
+
+function toggleSpellCheckingEnabled() {
+ enableInlineSpellCheck(!gSpellCheckingEnabled);
+}
+
+// This function is called either at startup (see InitEditor above), or when
+// the user clicks on one of the two menu items that allow them to toggle the
+// spellcheck feature (either context menu or Options menu).
+function enableInlineSpellCheck(aEnableInlineSpellCheck) {
+ let checker = GetCurrentEditorSpellChecker();
+ if (!checker) {
+ return;
+ }
+ if (gSpellCheckingEnabled != aEnableInlineSpellCheck) {
+ // If state of spellchecker is about to change, clear any pending observer.
+ spellCheckReadyObserver.removeObserver();
+ }
+
+ gSpellCheckingEnabled = checker.enableRealTimeSpell = aEnableInlineSpellCheck;
+ document
+ .getElementById("msgSubject")
+ .setAttribute("spellcheck", aEnableInlineSpellCheck);
+}
+
+function getMailToolbox() {
+ return document.getElementById("compose-toolbox");
+}
+
+/**
+ * Helper function to dispatch a CustomEvent to the attachmentbucket.
+ *
+ * @param aEventType the name of the event to fire.
+ * @param aData any detail data to pass to the CustomEvent.
+ */
+function dispatchAttachmentBucketEvent(aEventType, aData) {
+ gAttachmentBucket.dispatchEvent(
+ new CustomEvent(aEventType, {
+ bubbles: true,
+ cancelable: true,
+ detail: aData,
+ })
+ );
+}
+
+/** Update state of zoom type (text vs. full) menu item. */
+function UpdateFullZoomMenu() {
+ let menuItem = document.getElementById("menu_fullZoomToggle");
+ menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
+}
+
+/**
+ * Return the <editor> element of the mail compose window. The name is somewhat
+ * unfortunate; we need to maintain it since the zoom manager, view source and
+ * other functions still rely on it.
+ */
+function getBrowser() {
+ return document.getElementById("messageEditor");
+}
+
+function goUpdateMailMenuItems(commandset) {
+ for (let i = 0; i < commandset.children.length; i++) {
+ let commandID = commandset.children[i].getAttribute("id");
+ if (commandID) {
+ goUpdateCommand(commandID);
+ }
+ }
+}
+
+/**
+ * Object to handle message related notifications that are showing in a
+ * notificationbox below the composed message content.
+ */
+var gComposeNotificationBar = {
+ get brandBundle() {
+ delete this.brandBundle;
+ return (this.brandBundle = document.getElementById("brandBundle"));
+ },
+
+ setBlockedContent(aBlockedURI) {
+ let brandName = this.brandBundle.getString("brandShortName");
+ let buttonLabel = getComposeBundle().getString(
+ AppConstants.platform == "win"
+ ? "blockedContentPrefLabel"
+ : "blockedContentPrefLabelUnix"
+ );
+ let buttonAccesskey = getComposeBundle().getString(
+ AppConstants.platform == "win"
+ ? "blockedContentPrefAccesskey"
+ : "blockedContentPrefAccesskeyUnix"
+ );
+
+ let buttons = [
+ {
+ label: buttonLabel,
+ accessKey: buttonAccesskey,
+ popup: "blockedContentOptions",
+ callback(aNotification, aButton) {
+ return true; // keep notification open
+ },
+ },
+ ];
+
+ // The popup value is a space separated list of all the blocked urls.
+ let popup = document.getElementById("blockedContentOptions");
+ let urls = popup.value ? popup.value.split(" ") : [];
+ if (!urls.includes(aBlockedURI)) {
+ urls.push(aBlockedURI);
+ }
+ popup.value = urls.join(" ");
+
+ let msg = getComposeBundle().getFormattedString("blockedContentMessage", [
+ brandName,
+ brandName,
+ ]);
+ msg = PluralForm.get(urls.length, msg);
+
+ if (!this.isShowingBlockedContentNotification()) {
+ gComposeNotification.appendNotification(
+ "blockedContent",
+ {
+ label: msg,
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ buttons
+ );
+ } else {
+ gComposeNotification
+ .getNotificationWithValue("blockedContent")
+ .setAttribute("label", msg);
+ }
+ },
+
+ isShowingBlockedContentNotification() {
+ return !!gComposeNotification.getNotificationWithValue("blockedContent");
+ },
+
+ clearBlockedContentNotification() {
+ gComposeNotification.removeNotification(
+ gComposeNotification.getNotificationWithValue("blockedContent")
+ );
+ },
+
+ clearNotifications(aValue) {
+ gComposeNotification.removeAllNotifications(true);
+ },
+
+ /**
+ * Show a warning notification when a newly typed identity in the Form field
+ * doesn't match any existing identity.
+ *
+ * @param {string} identity - The name of the identity to add to the
+ * notification. Most likely an email address.
+ */
+ async setIdentityWarning(identity) {
+ // Bail out if we are already showing this type of notification.
+ if (gComposeNotification.getNotificationWithValue("identityWarning")) {
+ return;
+ }
+
+ gComposeNotification.appendNotification(
+ "identityWarning",
+ {
+ label: await document.l10n.formatValue(
+ "compose-missing-identity-warning",
+ {
+ identity,
+ }
+ ),
+ priority: gComposeNotification.PRIORITY_WARNING_HIGH,
+ },
+ null
+ );
+ },
+
+ clearIdentityWarning() {
+ let idWarning =
+ gComposeNotification.getNotificationWithValue("identityWarning");
+ if (idWarning) {
+ gComposeNotification.removeNotification(idWarning);
+ }
+ },
+};
+
+/**
+ * Populate the menuitems of what blocked content to unblock.
+ */
+function onBlockedContentOptionsShowing(aEvent) {
+ let urls = aEvent.target.value ? aEvent.target.value.split(" ") : [];
+
+ // Out with the old...
+ while (aEvent.target.lastChild) {
+ aEvent.target.lastChild.remove();
+ }
+
+ // ... and in with the new.
+ for (let url of urls) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute(
+ "label",
+ getComposeBundle().getFormattedString("blockedAllowResource", [url])
+ );
+ menuitem.setAttribute("crop", "center");
+ menuitem.setAttribute("value", url);
+ menuitem.setAttribute(
+ "oncommand",
+ "onUnblockResource(this.value, this.parentNode);"
+ );
+ aEvent.target.appendChild(menuitem);
+ }
+}
+
+/**
+ * Handle clicking the "Load <url>" in the blocked content notification bar.
+ *
+ * @param {string} aURL - the URL that was unblocked
+ * @param {Node} aNode - the node holding as value the URLs of the blocked
+ * resources in the message (space separated).
+ */
+function onUnblockResource(aURL, aNode) {
+ try {
+ loadBlockedImage(aURL);
+ } catch (e) {
+ // Couldn't load the referenced image.
+ console.error(e);
+ } finally {
+ // Remove it from the list on success and failure.
+ let urls = aNode.value.split(" ");
+ for (let i = 0; i < urls.length; i++) {
+ if (urls[i] == aURL) {
+ urls.splice(i, 1);
+ aNode.value = urls.join(" ");
+ if (urls.length == 0) {
+ gComposeNotificationBar.clearBlockedContentNotification();
+ }
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Convert the blocked content to a data URL and swap the src to that for the
+ * elements that were using it.
+ *
+ * @param {string} aURL - (necko) URL to unblock
+ * @param {Bool} aReturnDataURL - return data: URL instead of processing image
+ * @returns {string} the image as data: URL.
+ * @throw Error() if reading the data failed
+ */
+function loadBlockedImage(aURL, aReturnDataURL = false) {
+ let filename;
+ if (/^(file|chrome|moz-extension):/i.test(aURL)) {
+ filename = aURL.substr(aURL.lastIndexOf("/") + 1);
+ } else {
+ let fnMatch = /[?&;]filename=([^?&]+)/.exec(aURL);
+ filename = (fnMatch && fnMatch[1]) || "";
+ }
+ filename = decodeURIComponent(filename);
+ let uri = Services.io.newURI(aURL);
+ let contentType;
+ if (filename) {
+ try {
+ contentType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromURI(uri);
+ } catch (ex) {
+ contentType = "image/png";
+ }
+
+ if (!contentType.startsWith("image/")) {
+ // Unsafe to unblock this. It would just be garbage either way.
+ throw new Error(
+ "Won't unblock; URL=" + aURL + ", contentType=" + contentType
+ );
+ }
+ } else {
+ // Assuming image/png is the best we can do.
+ contentType = "image/png";
+ }
+ let channel = Services.io.newChannelFromURI(
+ uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ let inputStream = channel.open();
+ let stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ stream.setInputStream(inputStream);
+ let streamData = "";
+ try {
+ while (stream.available() > 0) {
+ streamData += stream.readBytes(stream.available());
+ }
+ } catch (e) {
+ stream.close();
+ throw new Error("Couldn't read all data from URL=" + aURL + " (" + e + ")");
+ }
+ stream.close();
+ let encoded = btoa(streamData);
+ let dataURL =
+ "data:" +
+ contentType +
+ (filename ? ";filename=" + encodeURIComponent(filename) : "") +
+ ";base64," +
+ encoded;
+
+ if (aReturnDataURL) {
+ return dataURL;
+ }
+
+ let editor = GetCurrentEditor();
+ for (let img of editor.document.images) {
+ if (img.src == aURL) {
+ img.src = dataURL; // Swap to data URL.
+ img.classList.remove("loading-internal");
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Update state of encrypted/signed toolbar buttons
+ */
+function showSendEncryptedAndSigned() {
+ let encToggle = document.getElementById("button-encryption");
+ if (encToggle) {
+ if (gSendEncrypted) {
+ encToggle.setAttribute("checked", "true");
+ } else {
+ encToggle.removeAttribute("checked");
+ }
+ }
+
+ let sigToggle = document.getElementById("button-signing");
+ if (sigToggle) {
+ if (gSendSigned) {
+ sigToggle.setAttribute("checked", "true");
+ } else {
+ sigToggle.removeAttribute("checked");
+ }
+ }
+
+ // Should button remain enabled? Identity might be unable to
+ // encrypt, but we might have kept button enabled after identity change.
+ let identityHasConfiguredSMIME =
+ isSmimeSigningConfigured() || isSmimeEncryptionConfigured();
+ let identityHasConfiguredOpenPGP = isPgpConfigured();
+ let e2eeNotConfigured =
+ !identityHasConfiguredOpenPGP && !identityHasConfiguredSMIME;
+
+ if (encToggle) {
+ encToggle.disabled = e2eeNotConfigured && !gSendEncrypted;
+ }
+ if (sigToggle) {
+ sigToggle.disabled = e2eeNotConfigured;
+ }
+}
+
+/**
+ * Look at the current encryption setting, and perform necessary
+ * automatic adjustments to related settings.
+ */
+function updateEncryptionDependencies() {
+ let canSign = gSelectedTechnologyIsPGP
+ ? isPgpConfigured()
+ : isSmimeSigningConfigured();
+
+ if (!canSign) {
+ gSendSigned = false;
+ gUserTouchedSendSigned = false;
+ } else if (!gSendEncrypted) {
+ if (!gUserTouchedSendSigned) {
+ gSendSigned = gCurrentIdentity.signMail;
+ }
+ } else if (!gUserTouchedSendSigned) {
+ gSendSigned = true;
+ }
+
+ // if (!gSendEncrypted) we don't need to change gEncryptSubject,
+ // it will be ignored anyway.
+ if (gSendEncrypted) {
+ if (!gUserTouchedEncryptSubject) {
+ gEncryptSubject = gCurrentIdentity.protectSubject;
+ }
+ }
+
+ if (!gSendSigned) {
+ if (!gUserTouchedAttachMyPubKey) {
+ gAttachMyPublicPGPKey = false;
+ }
+ } else if (!gUserTouchedAttachMyPubKey) {
+ gAttachMyPublicPGPKey = gCurrentIdentity.attachPgpKey;
+ }
+
+ if (!gSendEncrypted) {
+ clearRecipPillKeyIssues();
+ }
+
+ if (gSMFields && !gSelectedTechnologyIsPGP) {
+ gSMFields.requireEncryptMessage = gSendEncrypted;
+ gSMFields.signMessage = gSendSigned;
+ }
+
+ updateAttachMyPubKey();
+
+ updateEncryptedSubject();
+ showSendEncryptedAndSigned();
+
+ updateEncryptOptionsMenuElements();
+ checkEncryptedBccRecipients();
+}
+
+/**
+ * Listen to the click events on the compose window.
+ *
+ * @param {Event} event - The DOM Event
+ */
+function composeWindowOnClick(event) {
+ // Don't deselect pills if the click happened on another pill as the selection
+ // and focus change is handled by the pill itself. We also ignore clicks on
+ // toolbarbuttons, menus, and menu items. This will also prevent the unwanted
+ // deselection when opening the context menu on macOS.
+ if (
+ event.target?.tagName == "mail-address-pill" ||
+ event.target?.tagName == "toolbarbutton" ||
+ event.target?.tagName == "menu" ||
+ event.target?.tagName == "menuitem"
+ ) {
+ return;
+ }
+
+ document.getElementById("recipientsContainer").deselectAllPills();
+}
diff --git a/comm/mail/components/compose/content/addressingWidgetOverlay.js b/comm/mail/components/compose/content/addressingWidgetOverlay.js
new file mode 100644
index 0000000000..cee4b6889e
--- /dev/null
+++ b/comm/mail/components/compose/content/addressingWidgetOverlay.js
@@ -0,0 +1,1336 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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 MsgComposeCommands.js */
+/* import-globals-from ../../addrbook/content/abCommon.js */
+/* globals goDoCommand */ // From globalOverlay.js
+
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+var { DisplayNameUtils } = ChromeUtils.import(
+ "resource:///modules/DisplayNameUtils.jsm"
+);
+
+// Temporarily prevent repeated deletion key events in address rows or subject.
+// Prevent the keyboard shortcut for removing an empty address row (long
+// Backspace or Delete keypress) from affecting another row. Also, when a long
+// deletion keypress has just removed all text or all visible text from a row
+// input, prevent the ongoing keypress from removing the row.
+var gPreventRowDeletionKeysRepeat = false;
+
+/**
+ * Convert all the written recipients into string and store them into the
+ * msgCompFields array to be printed in the message header.
+ *
+ * @param {object} msgCompFields - An object to receive the recipients.
+ */
+function Recipients2CompFields(msgCompFields) {
+ if (!msgCompFields) {
+ throw new Error(
+ "Message Compose Error: msgCompFields is null (ExtractRecipients)"
+ );
+ }
+
+ let otherHeaders = Services.prefs
+ .getCharPref("mail.compose.other.header", "")
+ .split(",")
+ .map(h => h.trim())
+ .filter(Boolean);
+ for (let row of document.querySelectorAll(".address-row-raw")) {
+ let recipientType = row.dataset.recipienttype;
+ let headerValue = row.querySelector(".address-row-input").value.trim();
+ if (headerValue) {
+ msgCompFields.setRawHeader(recipientType, headerValue);
+ } else if (otherHeaders.includes(recipientType)) {
+ msgCompFields.deleteHeader(recipientType);
+ }
+ }
+
+ let getRecipientList = recipientType =>
+ Array.from(
+ document.querySelectorAll(
+ `.address-row[data-recipienttype="${recipientType}"] mail-address-pill`
+ ),
+ pill => {
+ // Expect each pill to contain exactly one address.
+ let { name, email } = MailServices.headerParser.makeFromDisplayAddress(
+ pill.fullAddress
+ )[0];
+ return MailServices.headerParser.makeMimeAddress(name, email);
+ }
+ ).join(",");
+
+ msgCompFields.to = getRecipientList("addr_to");
+ msgCompFields.cc = getRecipientList("addr_cc");
+ msgCompFields.bcc = getRecipientList("addr_bcc");
+ msgCompFields.replyTo = getRecipientList("addr_reply");
+ msgCompFields.newsgroups = getRecipientList("addr_newsgroups");
+ msgCompFields.followupTo = getRecipientList("addr_followup");
+}
+
+/**
+ * Replace the specified address row's pills with new ones generated by the
+ * given header value. The address row will be automatically shown if the header
+ * value is non-empty.
+ *
+ * @param {string} rowId - The id of the address row to set.
+ * @param {string} headerValue - The headerValue to create pills from.
+ * @param {boolean} multi - If the headerValue contains potentially multiple
+ * addresses and needs to be parsed to extract them.
+ * @param {boolean} [forceShow=false] - Whether to show the row, even if the
+ * given value is empty.
+ */
+function setAddressRowFromCompField(
+ rowId,
+ headerValue,
+ multi,
+ forceShow = false
+) {
+ let row = document.getElementById(rowId);
+ addressRowClearPills(row);
+
+ let value = multi
+ ? MailServices.headerParser.parseEncodedHeaderW(headerValue).join(", ")
+ : headerValue;
+
+ if (value || forceShow) {
+ addressRowSetVisibility(row, true);
+ }
+ if (value) {
+ let input = row.querySelector(".address-row-input");
+ input.value = value;
+ recipientAddPills(input, true);
+ }
+}
+
+/**
+ * Convert all the recipients coming from a message header into pills.
+ *
+ * @param {object} msgCompFields - An object containing all the recipients. If
+ * any property is not a string, it is ignored.
+ */
+function CompFields2Recipients(msgCompFields) {
+ if (msgCompFields) {
+ // Populate all the recipients with the proper values.
+ if (typeof msgCompFields.replyTo == "string") {
+ setAddressRowFromCompField(
+ "addressRowReply",
+ msgCompFields.replyTo,
+ true
+ );
+ }
+
+ if (typeof msgCompFields.to == "string") {
+ setAddressRowFromCompField("addressRowTo", msgCompFields.to, true);
+ }
+
+ if (typeof msgCompFields.cc == "string") {
+ setAddressRowFromCompField(
+ "addressRowCc",
+ msgCompFields.cc,
+ true,
+ gCurrentIdentity.doCc
+ );
+ }
+
+ if (typeof msgCompFields.bcc == "string") {
+ setAddressRowFromCompField(
+ "addressRowBcc",
+ msgCompFields.bcc,
+ true,
+ gCurrentIdentity.doBcc
+ );
+ }
+
+ if (typeof msgCompFields.newsgroups == "string") {
+ setAddressRowFromCompField(
+ "addressRowNewsgroups",
+ msgCompFields.newsgroups,
+ false
+ );
+ }
+
+ if (typeof msgCompFields.followupTo == "string") {
+ setAddressRowFromCompField(
+ "addressRowFollowup",
+ msgCompFields.followupTo,
+ true
+ );
+ }
+
+ // Add the sender to our spell check ignore list.
+ if (gCurrentIdentity) {
+ addRecipientsToIgnoreList(gCurrentIdentity.fullAddress);
+ }
+
+ // Trigger this method only after all the pills have been created.
+ onRecipientsChanged(true);
+ }
+}
+
+/**
+ * Update the recipients area UI to show News related fields and hide
+ * Mail related fields.
+ */
+function updateUIforNNTPAccount() {
+ // Hide the `mail-primary-input` field row if no pills have been created.
+ let mailContainer = document
+ .querySelector(".mail-primary-input")
+ .closest(".address-container");
+ if (mailContainer.querySelectorAll("mail-address-pill").length == 0) {
+ mailContainer
+ .closest(".address-row")
+ .querySelector(".remove-field-button")
+ .click();
+ }
+
+ // Show the closing label.
+ mailContainer
+ .closest(".address-row")
+ .querySelector(".remove-field-button").hidden = false;
+
+ // Show the `news-primary-input` field row if not already visible.
+ let newsContainer = document
+ .querySelector(".news-primary-input")
+ .closest(".address-row");
+ showAndFocusAddressRow(newsContainer.id);
+
+ // Hide the closing label.
+ newsContainer.querySelector(".remove-field-button").hidden = true;
+
+ // Prefer showing the buttons for news-show-row-menuitem items.
+ for (let item of document.querySelectorAll(".news-show-row-menuitem")) {
+ showAddressRowMenuItemSetPreferButton(item, true);
+ }
+
+ for (let item of document.querySelectorAll(".mail-show-row-menuitem")) {
+ showAddressRowMenuItemSetPreferButton(item, false);
+ }
+}
+
+/**
+ * Update the recipients area UI to show Mail related fields and hide
+ * News related fields. This method is called only if the UI was previously
+ * updated to accommodate a News account type.
+ */
+function updateUIforMailAccount() {
+ // Show the `mail-primary-input` field row if not already visible.
+ let mailContainer = document
+ .querySelector(".mail-primary-input")
+ .closest(".address-row");
+ showAndFocusAddressRow(mailContainer.id);
+
+ // Hide the closing label.
+ mailContainer.querySelector(".remove-field-button").hidden = true;
+
+ // Hide the `news-primary-input` field row if no pills have been created.
+ let newsContainer = document
+ .querySelector(".news-primary-input")
+ .closest(".address-row");
+ if (newsContainer.querySelectorAll("mail-address-pill").length == 0) {
+ newsContainer.querySelector(".remove-field-button").click();
+ }
+
+ // Show the closing label.
+ newsContainer.querySelector(".remove-field-button").hidden = false;
+
+ // Prefer showing the buttons for mail-show-row-menuitem items.
+ for (let item of document.querySelectorAll(".mail-show-row-menuitem")) {
+ showAddressRowMenuItemSetPreferButton(item, true);
+ }
+
+ for (let item of document.querySelectorAll(".news-show-row-menuitem")) {
+ showAddressRowMenuItemSetPreferButton(item, false);
+ }
+}
+
+/**
+ * Remove recipient pills from a specific addressing field based on full address
+ * matching. This is commonly used to clear previous Auto-CC/BCC recipients when
+ * loading a new identity.
+ *
+ * @param {object} msgCompFields - gMsgCompose.compFields, for helper functions.
+ * @param {string} recipientType - The type of recipients to remove,
+ * e.g. "addr_to" (recipient label id).
+ * @param {string} recipientsList - Comma-separated string containing recipients
+ * to be removed. May contain display names, and other commas therein. We only
+ * remove first exact match (full address).
+ */
+function awRemoveRecipients(msgCompFields, recipientType, recipientsList) {
+ if (!recipientType || !recipientsList) {
+ return;
+ }
+
+ let container;
+ switch (recipientType) {
+ case "addr_cc":
+ container = document.getElementById("ccAddrContainer");
+ break;
+ case "addr_bcc":
+ container = document.getElementById("bccAddrContainer");
+ break;
+ case "addr_reply":
+ container = document.getElementById("replyAddrContainer");
+ break;
+ case "addr_to":
+ container = document.getElementById("toAddrContainer");
+ break;
+ }
+
+ // Convert csv string of recipients to be deleted into full addresses array.
+ let recipientsArray = msgCompFields.splitRecipients(recipientsList, false);
+
+ // Remove first instance of specified recipients from specified container.
+ for (let recipientFullAddress of recipientsArray) {
+ let pill = container.querySelector(
+ `mail-address-pill[fullAddress="${recipientFullAddress}"]`
+ );
+ if (pill) {
+ pill.remove();
+ }
+ }
+
+ let addressRow = container.closest(`.address-row`);
+
+ // Remove entire address row if empty, no user input, and not type "addr_to".
+ if (
+ recipientType != "addr_to" &&
+ !container.querySelector(`mail-address-pill`) &&
+ !container.querySelector(`input[is="autocomplete-input"]`).value
+ ) {
+ addressRowSetVisibility(addressRow, false);
+ }
+
+ updateAriaLabelsOfAddressRow(addressRow);
+}
+
+/**
+ * Adds a batch of new rows matching recipientType and drops in the list of addresses.
+ *
+ * @param msgCompFields A nsIMsgCompFields object that is only used as a helper,
+ * it will not get the addresses appended.
+ * @param recipientType Type of recipient, e.g. "addr_to".
+ * @param recipientList A string of addresses to add.
+ */
+function awAddRecipients(msgCompFields, recipientType, recipientsList) {
+ if (!msgCompFields || !recipientsList) {
+ return;
+ }
+
+ addressRowAddRecipientsArray(
+ document.querySelector(
+ `.address-row[data-recipienttype="${recipientType}"]`
+ ),
+ msgCompFields.splitRecipients(recipientsList, false)
+ );
+}
+
+/**
+ * Adds a batch of new recipient pill matching recipientType and drops in the
+ * array of addresses.
+ *
+ * @param {Element} row - The row to add the addresses to.
+ * @param {string[]} addressArray - Recipient addresses (strings) to add.
+ * @param {boolean=false} select - If the newly generated pills should be
+ * selected.
+ */
+function addressRowAddRecipientsArray(row, addressArray, select = false) {
+ let addresses = [];
+ for (let addr of addressArray) {
+ addresses.push(...MailServices.headerParser.makeFromDisplayAddress(addr));
+ }
+
+ if (row.classList.contains("hidden")) {
+ showAndFocusAddressRow(row.id, true);
+ }
+
+ let recipientArea = document.getElementById("recipientsContainer");
+ let input = row.querySelector(".address-row-input");
+ for (let address of addresses) {
+ let pill = recipientArea.createRecipientPill(input, address);
+ if (select) {
+ pill.setAttribute("selected", "selected");
+ }
+ }
+
+ row
+ .querySelector(".address-container")
+ .classList.add("addressing-field-edited");
+
+ // Add the recipients to our spell check ignore list.
+ addRecipientsToIgnoreList(addressArray.join(", "));
+ updateAriaLabelsOfAddressRow(row);
+
+ if (row.id != "addressRowReply") {
+ onRecipientsChanged();
+ }
+}
+
+/**
+ * Find the autocomplete input when an address is dropped in the compose header.
+ *
+ * @param {XULElement} target - The element where an address was dropped.
+ * @param {string} recipient - The email address dragged by the user.
+ */
+function DropRecipient(target, recipient) {
+ let row;
+ if (target.classList.contains("address-row")) {
+ row = target;
+ } else if (target.dataset.addressRow) {
+ row = document.getElementById(target.dataset.addressRow);
+ } else {
+ row = target.closest(".address-row");
+ }
+ if (!row || row.classList.contains("address-row-raw")) {
+ return;
+ }
+
+ addressRowAddRecipientsArray(row, [recipient]);
+}
+
+// Returns the load context for the current window
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+/**
+ * Focus the next available address row's input. Otherwise, focus the "Subject"
+ * input.
+ *
+ * @param {Element} currentInput - The current input to search from.
+ */
+function focusNextAddressRow(currentInput) {
+ let addressRow = currentInput.closest(".address-row").nextElementSibling;
+ while (addressRow) {
+ if (focusAddressRowInput(addressRow)) {
+ return;
+ }
+ addressRow = addressRow.nextElementSibling;
+ }
+ focusSubjectInput();
+}
+
+/**
+ * Handle keydown events for other header input fields in the compose window.
+ * Only applies to rows created from mail.compose.other.header pref; no pills.
+ * Keep behaviour in sync with addressInputOnBeforeHandleKeyDown().
+ *
+ * @param {Event} event - The DOM keydown event.
+ */
+function otherHeaderInputOnKeyDown(event) {
+ let input = event.target;
+
+ switch (event.key) {
+ case " ":
+ // If the existing input value is empty string or whitespace only,
+ // prevent entering space and clear whitespace-only input text.
+ if (!input.value.trim()) {
+ event.preventDefault();
+ input.value = "";
+ }
+ break;
+
+ case "Enter":
+ // Break if modifier keys were used, to prevent hijacking unrelated
+ // keyboard shortcuts like Ctrl/Cmd+[Shift]+Enter for sending.
+ if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
+ break;
+ }
+
+ // Enter was pressed: 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 "Backspace":
+ case "Delete":
+ if (event.repeat && gPreventRowDeletionKeysRepeat) {
+ // Prevent repeated deletion keydown event if the flag is set.
+ event.preventDefault();
+ break;
+ }
+ // Enable repeated deletion in case of a non-repeated deletion keydown
+ // event, or if the flag is already false.
+ gPreventRowDeletionKeysRepeat = false;
+
+ if (
+ !event.repeat ||
+ input.value.trim() ||
+ input.selectionStart + input.selectionEnd ||
+ input
+ .closest(".address-row")
+ .querySelector(".remove-field-button[hidden]") ||
+ event.altKey
+ ) {
+ // Break if it is not a long deletion keypress, input still has text,
+ // or cursor selection is not at position 0 while deleting whitespace,
+ // to allow regular text deletion before we remove the row.
+ // Also break for non-removable rows with hidden [x] button, and if Alt
+ // key is pressed, to avoid interfering with undo shortcut Alt+Backspace.
+ break;
+ }
+ // Prevent event and set flag to prevent further unwarranted deletion in
+ // the adjacent 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
+ // deletion keydown event occurred, and it has an [x] button for removal.
+ hideAddressRowFromWithin(
+ input,
+ event.key == "Backspace" ? "previous" : "next"
+ );
+ break;
+ }
+}
+
+/**
+ * Handle keydown events for autocomplete address inputs in the compose window.
+ * Does not apply to rows created from mail.compose.other.header pref, which are
+ * handled with a subset of this function in otherHeaderInputOnKeyDown().
+ *
+ * @param {Event} event - The DOM keydown event.
+ */
+function addressInputOnBeforeHandleKeyDown(event) {
+ let 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();
+
+ let lastPill = input
+ .closest(".address-container")
+ .querySelector("mail-address-pill:last-of-type");
+ let 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.
+ let lastPillGlobal = mailRecipientsArea.querySelector(
+ "mail-address-pill:last-of-type"
+ );
+ if (lastPillGlobal) {
+ mailRecipientsArea.selectAllPills();
+ lastPillGlobal.focus();
+ }
+ break;
+
+ case " ":
+ case ",":
+ let 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();
+
+ let 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].
+ let 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;
+ }
+}
+
+/**
+ * Handle input events for all types of address inputs in the compose window.
+ *
+ * @param {Event} event - A DOM input event.
+ * @param {boolean} rawInput - A flag for plain text inputs created via
+ * mail.compose.other.header, which do not have autocompletion and pills.
+ */
+function addressInputOnInput(event, rawInput) {
+ let input = event.target;
+
+ if (
+ !input.value ||
+ (!input.value.trim() &&
+ input.selectionStart + input.selectionEnd == 0 &&
+ event.inputType == "deleteContentBackward")
+ ) {
+ // Temporarily disable repeated deletion to prevent premature
+ // removal of the current row if input text has just become empty or
+ // whitespace-only with cursor at position 0 from backwards deletion.
+ gPreventRowDeletionKeysRepeat = true;
+ }
+
+ if (rawInput) {
+ // For raw inputs, we are done.
+ return;
+ }
+ // Now handling only autocomplete inputs.
+
+ // Trigger onRecipientsChanged() for every input text change in order
+ // to properly update the "Send" button and trigger the save as draft
+ // prompt even before the creation of any pill.
+ onRecipientsChanged();
+
+ // Change the min size of the input field on input change only if the
+ // current width is smaller than 80% of its container's width
+ // to prevent overflow.
+ if (
+ input.clientWidth <
+ input.closest(".address-container").clientWidth * 0.8
+ ) {
+ document
+ .getElementById("recipientsContainer")
+ .resizeInputField(input, input.value.trim().length);
+ }
+}
+
+/**
+ * Add one or more <mail-address-pill> elements to the containing address row.
+ *
+ * @param {Element} input - Address input where "autocomplete-did-enter-text"
+ * was observed, and/or to whose containing address row pill(s) will be added.
+ * @param {boolean} [automatic=false] - Set to true if the change of recipients
+ * was invoked programmatically and should not be considered a change of
+ * message content.
+ */
+function recipientAddPills(input, automatic = false) {
+ if (!input.value.trim()) {
+ return;
+ }
+
+ let addresses = MailServices.headerParser.makeFromDisplayAddress(input.value);
+ let recipientArea = document.getElementById("recipientsContainer");
+
+ for (let address of addresses) {
+ recipientArea.createRecipientPill(input, address);
+ }
+
+ // Add the just added recipient address(es) to the spellcheck ignore list.
+ addRecipientsToIgnoreList(input.value.trim());
+
+ // Reset the input element.
+ input.removeAttribute("nomatch");
+ input.setAttribute("size", 1);
+ input.value = "";
+
+ // We need to detach the autocomplete Controller to prevent the input
+ // to be filled with the previously selected address when the "blur" event
+ // gets triggered.
+ input.detachController();
+ // If it was detached, attach it again to enable autocomplete.
+ if (!input.controller.input) {
+ input.attachController();
+ }
+
+ // Prevent triggering some methods if the pill creation was done automatically
+ // for example during the move of an existing pill between addressing fields.
+ if (!automatic) {
+ input
+ .closest(".address-container")
+ .classList.add("addressing-field-edited");
+ onRecipientsChanged();
+ }
+
+ updateAriaLabelsOfAddressRow(input.closest(".address-row"));
+}
+
+/**
+ * Remove all <mail-address-pill> elements from the containing address row.
+ *
+ * @param {Element} row - The address row to clear.
+ */
+function addressRowClearPills(row) {
+ for (let pill of row.querySelectorAll(
+ ".address-container mail-address-pill"
+ )) {
+ pill.remove();
+ }
+ updateAriaLabelsOfAddressRow(row);
+}
+
+/**
+ * Handle focus event of address inputs: Force a focused styling on the closest
+ * address container of the currently focused input element.
+ *
+ * @param {Element} input - The address input element receiving focus.
+ */
+function addressInputOnFocus(input) {
+ input.closest(".address-container").setAttribute("focused", "true");
+}
+
+/**
+ * Handle blur event of address inputs: Remove focused styling from the closest
+ * address container and create address pills if valid recipients were written.
+ *
+ * @param {Element} input - The input element losing focus.
+ */
+function addressInputOnBlur(input) {
+ input.closest(".address-container").removeAttribute("focused");
+
+ // If the input is still the active element after blur (when switching to
+ // another window), return to prevent autocompletion and pillification
+ // and let the user continue editing the address later where he left.
+ if (document.activeElement == input) {
+ return;
+ }
+
+ // For other headers aka raw input, trim and we are done.
+ if (input.getAttribute("is") != "autocomplete-input") {
+ input.value = input.value.trim();
+ return;
+ }
+
+ let address = input.value.trim();
+ if (!address) {
+ // If input is empty or whitespace only, clear input to remove any leftover
+ // whitespace, reset the input size, and return.
+ input.value = "";
+ input.setAttribute("size", 1);
+ return;
+ }
+
+ if (input.forceComplete && input.mController.matchCount >= 1) {
+ // If input.forceComplete is true and there are autocomplete matches,
+ // we need to call the inbuilt Enter handler to force the input text
+ // to the best autocomplete match because we've set input._dontBlur.
+ input.mController.handleEnter(true);
+ return;
+ }
+
+ // Otherwise, try to parse the input text as comma-separated recipients and
+ // convert them into recipient pills.
+ let listNames = MimeParser.parseHeaderField(
+ address,
+ MimeParser.HEADER_ADDRESS
+ );
+ let isMailingList =
+ listNames.length > 0 &&
+ MailServices.ab.mailListNameExists(listNames[0].name);
+
+ if (
+ address &&
+ (isValidAddress(address) ||
+ isMailingList ||
+ input.classList.contains("news-input"))
+ ) {
+ recipientAddPills(input);
+ }
+
+ // Trim any remaining input for which we didn't create a pill.
+ if (input.value.trim()) {
+ input.value = input.value.trim();
+ }
+}
+
+/**
+ * Trigger the startEditing() method of the mail-address-pill element.
+ *
+ * @param {XULlement} element - The element from which the context menu was
+ * opened.
+ * @param {Event} event - The DOM event.
+ */
+function editAddressPill(element, event) {
+ document
+ .getElementById("recipientsContainer")
+ .startEditing(element.closest("mail-address-pill"), event);
+}
+
+/**
+ * Expands all the selected mailing list pills into their composite addresses.
+ *
+ * @param {XULlement} element - The element from which the context menu was
+ * opened.
+ */
+function expandList(element) {
+ let pill = element.closest("mail-address-pill");
+ if (pill.isMailList) {
+ let addresses = [];
+ for (let currentPill of pill.parentNode.querySelectorAll(
+ "mail-address-pill"
+ )) {
+ if (currentPill == pill) {
+ let dir = MailServices.ab.getDirectory(pill.listURI);
+ if (dir) {
+ for (let card of dir.childCards) {
+ addresses.push(makeMailboxObjectFromCard(card));
+ }
+ }
+ } else {
+ addresses.push(currentPill.fullAddress);
+ }
+ }
+ let row = pill.closest(".address-row");
+ addressRowClearPills(row);
+ addressRowAddRecipientsArray(row, addresses, false);
+ }
+}
+
+/**
+ * Handle the disabling of context menu items according to the types and count
+ * of selected pills.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function onPillPopupShowing(event) {
+ let menu = event.target;
+ // Reset previously hidden menuitems.
+ for (let menuitem of menu.querySelectorAll(
+ ".pill-action-move, .pill-action-edit"
+ )) {
+ menuitem.hidden = false;
+ }
+
+ let recipientsContainer = document.getElementById("recipientsContainer");
+
+ // Check if the pill where the context menu was originated is not selected.
+ let pill = event.explicitOriginalTarget.closest("mail-address-pill");
+ if (!pill.hasAttribute("selected")) {
+ recipientsContainer.deselectAllPills();
+ pill.setAttribute("selected", "selected");
+ }
+
+ let allSelectedPills = recipientsContainer.getAllSelectedPills();
+ // If more than one pill is selected, hide the editing item.
+ if (recipientsContainer.getAllSelectedPills().length > 1) {
+ menu.querySelector("#editAddressPill").hidden = true;
+ }
+
+ // Update the recipient type in the menu label of #menu_selectAllSiblingPills.
+ let type = pill
+ .closest(".address-row")
+ .querySelector(".address-label-container > label").value;
+ document.l10n.setAttributes(
+ menu.querySelector("#menu_selectAllSiblingPills"),
+ "pill-action-select-all-sibling-pills",
+ { type }
+ );
+
+ // Hide the `Expand List` menuitem and the preceding menuseparator if not all
+ // selected pills are mailing lists.
+ let isNotMailingList = [...allSelectedPills].some(pill => !pill.isMailList);
+ menu.querySelector("#expandList").hidden = isNotMailingList;
+ menu.querySelector("#pillContextBeforeExpandListSeparator").hidden =
+ isNotMailingList;
+
+ // If any Newsgroup or Followup pill is selected, hide all move actions.
+ if (
+ recipientsContainer.querySelector(
+ ":is(#addressRowNewsgroups, #addressRowFollowup) " +
+ "mail-address-pill[selected]"
+ )
+ ) {
+ for (let menuitem of menu.querySelectorAll(".pill-action-move")) {
+ menuitem.hidden = true;
+ }
+ // Hide the menuseparator before the move items, as there's nothing below.
+ menu.querySelector("#pillContextBeforeMoveItemsSeparator").hidden = true;
+ return;
+ }
+ // Show the menuseparator before the move items as no Newsgroup or Followup
+ // pill is selected.
+ menu.querySelector("#pillContextBeforeMoveItemsSeparator").hidden = false;
+
+ let selectedType = "";
+ // Check if all selected pills are in the same address row.
+ for (let row of recipientsContainer.querySelectorAll(
+ ".address-row:not(.hidden)"
+ )) {
+ // Check if there's at least one selected pill in the address row.
+ let selectedPill = row.querySelector("mail-address-pill[selected]");
+ if (!selectedPill) {
+ continue;
+ }
+ // Return if we already have a selectedType: More than one type selected.
+ if (selectedType) {
+ return;
+ }
+ selectedType = row.dataset.recipienttype;
+ }
+
+ // All selected pills are of the same type, hide the type's move action.
+ switch (selectedType) {
+ case "addr_to":
+ menu.querySelector("#moveAddressPillTo").hidden = true;
+ break;
+
+ case "addr_cc":
+ menu.querySelector("#moveAddressPillCc").hidden = true;
+ break;
+
+ case "addr_bcc":
+ menu.querySelector("#moveAddressPillBcc").hidden = true;
+ break;
+ }
+}
+
+/**
+ * Show the specified address row and focus its input. If showing the address
+ * row is disabled, the focus is not changed.
+ *
+ * @param {string} rowId - The id of the row to show.
+ */
+function showAndFocusAddressRow(rowId) {
+ let row = document.getElementById(rowId);
+ if (addressRowSetVisibility(row, true)) {
+ row.querySelector(".address-row-input").focus();
+ }
+}
+
+/**
+ * Set the visibility of an address row (Cc, Bcc, etc.).
+ *
+ * @param {Element} row - The address row.
+ * @param {boolean} [show=true] - Whether to show the row or hide it.
+ *
+ * @returns {boolean} - Whether the visibility was set.
+ */
+function addressRowSetVisibility(row, show) {
+ let menuItem = document.getElementById(row.dataset.showSelfMenuitem);
+ if (show && menuItem.hasAttribute("disabled")) {
+ return false;
+ }
+
+ // Show/hide the row and hide/show the menuitem or button
+ row.classList.toggle("hidden", !show);
+ showAddressRowMenuItemSetVisibility(menuItem, !show);
+ return true;
+}
+
+/**
+ * Set the visibility of a menu item that shows an address row.
+ *
+ * @param {Element} menuItem - The menu item.
+ * @param {boolean} [show=true] - Whether to show the item or hide it.
+ */
+function showAddressRowMenuItemSetVisibility(menuItem, show) {
+ let buttonId = menuItem.dataset.buttonId;
+ let button = buttonId && document.getElementById(buttonId);
+ if (button && menuItem.dataset.preferButton == "true") {
+ button.hidden = !show;
+ // Make sure the menuItem is never shown.
+ menuItem.hidden = true;
+ } else {
+ menuItem.hidden = !show;
+ if (button) {
+ button.hidden = true;
+ }
+ }
+
+ updateRecipientsVisibility();
+}
+
+/**
+ * Set whether a menu item that shows an address row should prefer being
+ * displayed as the button specified by its "data-button-id" attribute, if it
+ * has one.
+ *
+ * @param {Element} menuItem - The menu item.
+ * @param {boolean} preferButton - Whether to prefer showing the button rather
+ * than the menu item.
+ */
+function showAddressRowMenuItemSetPreferButton(menuItem, preferButton) {
+ let buttonId = menuItem.dataset.buttonId;
+ if (!buttonId || menuItem.dataset.preferButton == String(preferButton)) {
+ return;
+ }
+ let button = document.getElementById(buttonId);
+
+ menuItem.dataset.preferButton = preferButton;
+ if (preferButton) {
+ button.hidden = menuItem.hidden;
+ menuItem.hidden = true;
+ } else {
+ menuItem.hidden = button.hidden;
+ button.hidden = true;
+ }
+
+ updateRecipientsVisibility();
+}
+
+/**
+ * Hide or show the menu button for the extra recipients based on the current
+ * hidden status of menuitems and buttons.
+ */
+function updateRecipientsVisibility() {
+ document.getElementById("extraAddressRowsMenuButton").hidden =
+ !document.querySelector("#extraAddressRowsMenu > :not([hidden])");
+
+ let buttonbox = document.getElementById("extraAddressRowsArea");
+ // Toggle the class to show/hide the pseudo element separator
+ // of the msgIdentity field.
+ buttonbox.classList.toggle(
+ "addressingWidget-separator",
+ !!buttonbox.querySelector("button:not([hidden])")
+ );
+}
+
+/**
+ * Hide the container row of a recipient (Cc, Bcc, etc.).
+ * The container can't be hidden if previously typed addresses are listed.
+ *
+ * @param {Element} element - A descendant element of the row to be hidden (or
+ * the row itself), usually the [x] label when triggered, or an empty address
+ * input upon Backspace or Del keydown.
+ * @param {("next"|"previous")} [focusType="next"] - How to move focus after
+ * hiding the address row: try to focus the input of an available next sibling
+ * row (for [x] or DEL) or previous sibling row (for BACKSPACE).
+ */
+function hideAddressRowFromWithin(element, focusType = "next") {
+ let addressRow = element.closest(".address-row");
+
+ // Prevent address row removal when sending (disable-on-send).
+ if (
+ addressRow
+ .querySelector(".address-container")
+ .classList.contains("disable-container")
+ ) {
+ return;
+ }
+
+ let pills = addressRow.querySelectorAll("mail-address-pill");
+ let isEdited = addressRow
+ .querySelector(".address-container")
+ .classList.contains("addressing-field-edited");
+
+ // Ask the user to confirm the removal of all the typed addresses if the field
+ // holds addressing pills and has been previously edited.
+ if (isEdited && pills.length) {
+ let fieldName = addressRow.querySelector(
+ ".address-label-container > label"
+ );
+ let confirmTitle = getComposeBundle().getFormattedString(
+ "confirmRemoveRecipientRowTitle2",
+ [fieldName.value]
+ );
+ let confirmBody = getComposeBundle().getFormattedString(
+ "confirmRemoveRecipientRowBody2",
+ [fieldName.value]
+ );
+ let confirmButton = getComposeBundle().getString(
+ "confirmRemoveRecipientRowButton"
+ );
+
+ let result = Services.prompt.confirmEx(
+ window,
+ confirmTitle,
+ confirmBody,
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL,
+ confirmButton,
+ null,
+ null,
+ null,
+ {}
+ );
+ if (result == 1) {
+ return;
+ }
+ }
+
+ for (let pill of pills) {
+ pill.remove();
+ }
+
+ // Reset the original input.
+ let input = addressRow.querySelector(".address-row-input");
+ input.value = "";
+
+ addressRowSetVisibility(addressRow, false);
+
+ // Update the Send button only if the content was previously changed.
+ if (isEdited) {
+ onRecipientsChanged(true);
+ }
+ updateAriaLabelsOfAddressRow(addressRow);
+
+ // Move focus to the next focusable address input field.
+ let addressRowSibling =
+ focusType == "next"
+ ? getNextSibling(addressRow, ".address-row:not(.hidden)")
+ : getPreviousSibling(addressRow, ".address-row:not(.hidden)");
+
+ if (addressRowSibling) {
+ addressRowSibling.querySelector(".address-row-input").focus();
+ return;
+ }
+ // Otherwise move focus to the subject field or to the first available input.
+ let fallbackFocusElement =
+ focusType == "next"
+ ? document.getElementById("msgSubject")
+ : getNextSibling(addressRow, ".address-row:not(.hidden)").querySelector(
+ ".address-row-input"
+ );
+ fallbackFocusElement.focus();
+}
+
+/**
+ * Handle the click event on the close label of an address row.
+ *
+ * @param {Event} event - The DOM click event.
+ */
+function closeLabelOnClick(event) {
+ hideAddressRowFromWithin(event.target);
+}
+
+function extraAddressRowsMenuOpened() {
+ document
+ .getElementById("extraAddressRowsMenuButton")
+ .setAttribute("aria-expanded", "true");
+}
+
+function extraAddressRowsMenuClosed() {
+ document
+ .getElementById("extraAddressRowsMenuButton")
+ .setAttribute("aria-expanded", "false");
+}
+
+/**
+ * Show the menu for extra address rows (extraAddressRowsMenu).
+ */
+function openExtraAddressRowsMenu() {
+ let button = document.getElementById("extraAddressRowsMenuButton");
+ let menu = document.getElementById("extraAddressRowsMenu");
+ // NOTE: menu handlers handle the aria-expanded state of the button.
+ menu.openPopup(button, "after_end", 8, 0);
+}
diff --git a/comm/mail/components/compose/content/bigFileObserver.js b/comm/mail/components/compose/content/bigFileObserver.js
new file mode 100644
index 0000000000..f741af7afa
--- /dev/null
+++ b/comm/mail/components/compose/content/bigFileObserver.js
@@ -0,0 +1,368 @@
+/* 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/. */
+
+/* global MozElements */
+
+/* import-globals-from MsgComposeCommands.js */
+
+var { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+
+var kUploadNotificationValue = "bigAttachmentUploading";
+var kPrivacyWarningNotificationValue = "bigAttachmentPrivacyWarning";
+
+var gBigFileObserver = {
+ bigFiles: [],
+ sessionHidden: false,
+ privacyWarned: false,
+
+ get hidden() {
+ return (
+ this.sessionHidden ||
+ !Services.prefs.getBoolPref("mail.cloud_files.enabled") ||
+ !Services.prefs.getBoolPref("mail.compose.big_attachments.notify") ||
+ Services.io.offline
+ );
+ },
+
+ hide(aPermanent) {
+ if (aPermanent) {
+ Services.prefs.setBoolPref("mail.compose.big_attachments.notify", false);
+ } else {
+ this.sessionHidden = true;
+ }
+ },
+
+ init() {
+ let bucket = document.getElementById("attachmentBucket");
+ bucket.addEventListener("attachments-added", this);
+ bucket.addEventListener("attachments-removed", this);
+ bucket.addEventListener("attachment-converted-to-regular", this);
+ bucket.addEventListener("attachment-uploading", this);
+ bucket.addEventListener("attachment-uploaded", this);
+ bucket.addEventListener("attachment-upload-failed", this);
+
+ this.sessionHidden = false;
+ this.privacyWarned = false;
+ this.bigFiles = [];
+ },
+
+ handleEvent(event) {
+ if (this.hidden) {
+ return;
+ }
+
+ switch (event.type) {
+ case "attachments-added":
+ this.bigFileTrackerAdd(event.detail);
+ break;
+ case "attachments-removed":
+ this.bigFileTrackerRemove(event.detail);
+ this.checkAndHidePrivacyNotification();
+ break;
+ case "attachment-converted-to-regular":
+ this.checkAndHidePrivacyNotification();
+ break;
+ case "attachment-uploading":
+ // Remove the currently uploading item from bigFiles, to remove the big
+ // file notification already during upload.
+ this.bigFileTrackerRemove([event.detail]);
+ this.updateUploadingNotification();
+ break;
+ case "attachment-upload-failed":
+ this.updateUploadingNotification();
+ break;
+ case "attachment-uploaded":
+ this.updateUploadingNotification();
+ if (this.uploadsInProgress == 0) {
+ this.showPrivacyNotification();
+ }
+ break;
+ default:
+ // Do not update the notification for other events.
+ return;
+ }
+
+ this.updateBigFileNotification();
+ },
+
+ bigFileTrackerAdd(aAttachments) {
+ let threshold =
+ Services.prefs.getIntPref("mail.compose.big_attachments.threshold_kb") *
+ 1024;
+
+ for (let attachment of aAttachments) {
+ if (attachment.size >= threshold && !attachment.sendViaCloud) {
+ this.bigFiles.push(attachment);
+ }
+ }
+ },
+
+ bigFileTrackerRemove(aAttachments) {
+ for (let attachment of aAttachments) {
+ let index = this.bigFiles.findIndex(e => e.url == attachment.url);
+ if (index != -1) {
+ this.bigFiles.splice(index, 1);
+ }
+ }
+ },
+
+ formatString(key, replacements, plural) {
+ let str = getComposeBundle().getString(key);
+ if (plural !== undefined) {
+ str = PluralForm.get(plural, str);
+ }
+ if (replacements !== undefined) {
+ for (let i = 0; i < replacements.length; i++) {
+ str = str.replace("#" + (i + 1), replacements[i]);
+ }
+ }
+ return str;
+ },
+
+ updateBigFileNotification() {
+ let bigFileNotification =
+ gComposeNotification.getNotificationWithValue("bigAttachment");
+ if (this.bigFiles.length) {
+ if (bigFileNotification) {
+ bigFileNotification.label = this.formatString(
+ "bigFileDescription",
+ [this.bigFiles.length],
+ this.bigFiles.length
+ );
+ return;
+ }
+
+ let buttons = [
+ {
+ label: getComposeBundle().getString("learnMore.label"),
+ accessKey: getComposeBundle().getString("learnMore.accesskey"),
+ callback: this.openLearnMore.bind(this),
+ },
+ {
+ label: this.formatString("bigFileShare.label", []),
+ accessKey: this.formatString("bigFileShare.accesskey"),
+ callback: this.convertAttachments.bind(this),
+ },
+ {
+ label: this.formatString("bigFileAttach.label", []),
+ accessKey: this.formatString("bigFileAttach.accesskey"),
+ callback: this.hideBigFileNotification.bind(this),
+ },
+ ];
+
+ let msg = this.formatString(
+ "bigFileDescription",
+ [this.bigFiles.length],
+ this.bigFiles.length
+ );
+
+ bigFileNotification = gComposeNotification.appendNotification(
+ "bigAttachment",
+ {
+ label: msg,
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ buttons
+ );
+ } else if (bigFileNotification) {
+ gComposeNotification.removeNotification(bigFileNotification);
+ }
+ },
+
+ openLearnMore() {
+ let url = Services.prefs.getCharPref("mail.cloud_files.learn_more_url");
+ openContentTab(url);
+ return true;
+ },
+
+ convertAttachments() {
+ let account;
+ let accounts = cloudFileAccounts.configuredAccounts;
+
+ if (accounts.length == 1) {
+ account = accounts[0];
+ } else if (accounts.length > 1) {
+ // We once used Services.prompt.select for this UI, but it doesn't support displaying an
+ // icon for each item. The following code does the same thing with a replacement dialog.
+ let { PromptUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromptUtils.sys.mjs"
+ );
+
+ let names = accounts.map(i => cloudFileAccounts.getDisplayName(i));
+ let icons = accounts.map(i => i.iconURL);
+ let args = {
+ promptType: "select",
+ title: this.formatString("bigFileChooseAccount.title"),
+ text: this.formatString("bigFileChooseAccount.text"),
+ list: names,
+ icons,
+ selected: -1,
+ ok: false,
+ };
+
+ let propBag = PromptUtils.objectToPropBag(args);
+ openDialog(
+ "chrome://messenger/content/cloudfile/selectDialog.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ PromptUtils.propBagToObject(propBag, args);
+
+ if (args.ok) {
+ account = accounts[args.selected];
+ }
+ } else {
+ openPreferencesTab("paneCompose", "compositionAttachmentsCategory");
+ return true;
+ }
+
+ if (account) {
+ convertToCloudAttachment(this.bigFiles, account);
+ }
+
+ return false;
+ },
+
+ hideBigFileNotification() {
+ let never = {};
+ if (
+ Services.prompt.confirmCheck(
+ window,
+ this.formatString("bigFileHideNotification.title"),
+ this.formatString("bigFileHideNotification.text"),
+ this.formatString("bigFileHideNotification.check"),
+ never
+ )
+ ) {
+ this.hide(never.value);
+ return false;
+ }
+ return true;
+ },
+
+ updateUploadingNotification() {
+ // We will show the uploading notification for a minimum of 2.5 seconds
+ // seconds.
+ const kThreshold = 2500; // milliseconds
+
+ if (
+ !Services.prefs.getBoolPref(
+ "mail.compose.big_attachments.insert_notification"
+ )
+ ) {
+ return;
+ }
+
+ let activeUploads = this.uploadsInProgress;
+ let notification = gComposeNotification.getNotificationWithValue(
+ kUploadNotificationValue
+ );
+
+ if (activeUploads == 0) {
+ if (notification) {
+ // Check the timestamp that we stashed in the timeout field of the
+ // notification...
+ let now = Date.now();
+ if (now >= notification.timeout) {
+ gComposeNotification.removeNotification(notification);
+ } else {
+ setTimeout(function () {
+ gComposeNotification.removeNotification(notification);
+ }, notification.timeout - now);
+ }
+ }
+ return;
+ }
+
+ let message = this.formatString("cloudFileUploadingNotification");
+ message = PluralForm.get(activeUploads, message);
+
+ if (notification) {
+ notification.label = message;
+ return;
+ }
+
+ let showUploadButton = {
+ accessKey: this.formatString(
+ "stopShowingUploadingNotification.accesskey"
+ ),
+ label: this.formatString("stopShowingUploadingNotification.label"),
+ callback(aNotificationBar, aButton) {
+ Services.prefs.setBoolPref(
+ "mail.compose.big_attachments.insert_notification",
+ false
+ );
+ },
+ };
+ notification = gComposeNotification.appendNotification(
+ kUploadNotificationValue,
+ {
+ label: message,
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ [showUploadButton]
+ );
+ notification.timeout = Date.now() + kThreshold;
+ },
+
+ hidePrivacyNotification() {
+ this.privacyWarned = false;
+ let notification = gComposeNotification.getNotificationWithValue(
+ kPrivacyWarningNotificationValue
+ );
+
+ if (notification) {
+ gComposeNotification.removeNotification(notification);
+ }
+ },
+
+ checkAndHidePrivacyNotification() {
+ if (
+ !gAttachmentBucket.itemChildren.find(
+ item => item.attachment && item.attachment.sendViaCloud
+ )
+ ) {
+ this.hidePrivacyNotification();
+ }
+ },
+
+ showPrivacyNotification() {
+ if (this.privacyWarned) {
+ return;
+ }
+ this.privacyWarned = true;
+
+ let notification = gComposeNotification.getNotificationWithValue(
+ kPrivacyWarningNotificationValue
+ );
+
+ if (notification) {
+ return;
+ }
+
+ let message = this.formatString("cloudFilePrivacyNotification");
+ gComposeNotification.appendNotification(
+ kPrivacyWarningNotificationValue,
+ {
+ label: message,
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+ },
+
+ get uploadsInProgress() {
+ let items = [...document.getElementById("attachmentBucket").itemChildren];
+ return items.filter(e => e.uploading).length;
+ },
+};
+
+window.addEventListener(
+ "compose-window-init",
+ gBigFileObserver.init.bind(gBigFileObserver),
+ true
+);
diff --git a/comm/mail/components/compose/content/cloudAttachmentLinkManager.js b/comm/mail/components/compose/content/cloudAttachmentLinkManager.js
new file mode 100644
index 0000000000..9693f1aa8d
--- /dev/null
+++ b/comm/mail/components/compose/content/cloudAttachmentLinkManager.js
@@ -0,0 +1,758 @@
+/* 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 MsgComposeCommands.js */
+
+let { MsgUtils } = ChromeUtils.import(
+ "resource:///modules/MimeMessageUtils.jsm"
+);
+
+var gCloudAttachmentLinkManager = {
+ init() {
+ this.cloudAttachments = [];
+
+ let bucket = document.getElementById("attachmentBucket");
+ bucket.addEventListener("attachments-removed", this);
+ bucket.addEventListener("attachment-converted-to-regular", this);
+ bucket.addEventListener("attachment-uploaded", this);
+ bucket.addEventListener("attachment-moved", this);
+ bucket.addEventListener("attachment-renamed", this);
+
+ // If we're restoring a draft that has some attachments,
+ // check to see if any of them are marked to be sent via
+ // cloud, and if so, add them to our list.
+ for (let i = 0; i < bucket.getRowCount(); ++i) {
+ let attachment = bucket.getItemAtIndex(i).attachment;
+ if (attachment && attachment.sendViaCloud) {
+ this.cloudAttachments.push(attachment);
+ }
+ }
+
+ gMsgCompose.RegisterStateListener(this);
+ },
+
+ NotifyComposeFieldsReady() {},
+ NotifyComposeBodyReady() {},
+ ComposeProcessDone() {},
+ SaveInFolderDone() {},
+
+ async handleEvent(event) {
+ let mailDoc = document.getElementById("messageEditor").contentDocument;
+
+ if (
+ event.type == "attachment-renamed" ||
+ event.type == "attachment-moved"
+ ) {
+ let cloudFileUpload = event.target.cloudFileUpload;
+ let items = [];
+
+ let list = mailDoc.getElementById("cloudAttachmentList");
+ if (list) {
+ items = list.getElementsByClassName("cloudAttachmentItem");
+ }
+
+ for (let item of items) {
+ // The original attachment is stored in the events detail property.
+ if (item.dataset.contentLocation == event.detail.contentLocation) {
+ item.replaceWith(await this._createNode(mailDoc, cloudFileUpload));
+ }
+ }
+ if (event.type == "attachment-moved") {
+ await this._updateServiceProviderLinks(mailDoc);
+ }
+ } else if (event.type == "attachment-uploaded") {
+ if (this.cloudAttachments.length == 0) {
+ this._insertHeader(mailDoc);
+ }
+
+ let cloudFileUpload = event.target.cloudFileUpload;
+ let attachment = event.target.attachment;
+ this.cloudAttachments.push(attachment);
+ await this._insertItem(mailDoc, cloudFileUpload);
+ } else if (
+ event.type == "attachments-removed" ||
+ event.type == "attachment-converted-to-regular"
+ ) {
+ let items = [];
+ let list = mailDoc.getElementById("cloudAttachmentList");
+ if (list) {
+ items = list.getElementsByClassName("cloudAttachmentItem");
+ }
+
+ let attachments = Array.isArray(event.detail)
+ ? event.detail
+ : [event.detail];
+ for (let attachment of attachments) {
+ // Remove the attachment from the message body.
+ if (list) {
+ for (let item of items) {
+ if (item.dataset.contentLocation == attachment.contentLocation) {
+ item.remove();
+ }
+ }
+ }
+
+ // Now, remove the attachment from our internal list.
+ let index = this.cloudAttachments.indexOf(attachment);
+ if (index != -1) {
+ this.cloudAttachments.splice(index, 1);
+ }
+ }
+
+ await this._updateAttachmentCount(mailDoc);
+ await this._updateServiceProviderLinks(mailDoc);
+
+ if (items.length == 0) {
+ if (list) {
+ list.remove();
+ }
+ this._removeRoot(mailDoc);
+ }
+ }
+ },
+
+ /**
+ * Removes the root node for an attachment list in an HTML email.
+ *
+ * @param {Document} aDocument - the document to remove the root node from
+ */
+ _removeRoot(aDocument) {
+ let header = aDocument.getElementById("cloudAttachmentListRoot");
+ if (header) {
+ header.remove();
+ }
+ },
+
+ /**
+ * Given some node, returns the textual HTML representation for the node
+ * and its children.
+ *
+ * @param {Document} aDocument - the document that the node is embedded in
+ * @param {DOMNode} aNode - the node to get the textual representation from
+ */
+ _getHTMLRepresentation(aDocument, aNode) {
+ let tmp = aDocument.createElement("p");
+ tmp.appendChild(aNode);
+ return tmp.innerHTML;
+ },
+
+ /**
+ * Returns the plain text equivalent of the given HTML markup, ready to be
+ * inserted into a compose editor.
+ *
+ * @param {string} aMarkup - the HTML markup that should be converted
+ */
+ _getTextRepresentation(aMarkup) {
+ return MsgUtils.convertToPlainText(aMarkup, true).replaceAll("\r\n", "\n");
+ },
+
+ /**
+ * Generates an appropriately styled link.
+ *
+ * @param {Document} aDocument - the document to append the link to - doesn't
+ * actually get appended, but is used to generate the anchor node
+ * @param {string} aContent - the textual content of the link
+ * @param {string} aHref - the HREF attribute for the generated link
+ * @param {string} aColor - the CSS color string for the link
+ */
+ _generateLink(aDocument, aContent, aHref, aColor) {
+ let link = aDocument.createElement("a");
+ link.href = aHref;
+ link.textContent = aContent;
+ link.style.cssText = `color: ${aColor} !important`;
+ return link;
+ },
+
+ _findInsertionPoint(aDocument) {
+ let mailBody = aDocument.querySelector("body");
+ let editor = GetCurrentEditor();
+ let selection = editor.selection;
+
+ let childNodes = mailBody.childNodes;
+ let childToInsertAfter, childIndex;
+
+ // First, search for any text nodes that are immediate children of
+ // the body. If we find any, we'll insert after those.
+ for (childIndex = childNodes.length - 1; childIndex >= 0; childIndex--) {
+ if (childNodes[childIndex].nodeType == Node.TEXT_NODE) {
+ childToInsertAfter = childNodes[childIndex];
+ break;
+ }
+ }
+
+ if (childIndex != -1) {
+ selection.collapse(
+ childToInsertAfter,
+ childToInsertAfter.nodeValue ? childToInsertAfter.nodeValue.length : 0
+ );
+ if (
+ childToInsertAfter.nodeValue &&
+ childToInsertAfter.nodeValue.length > 0
+ ) {
+ editor.insertLineBreak();
+ }
+ editor.insertLineBreak();
+ return;
+ }
+
+ // If there's a signature, let's get a hold of it now.
+ let signature = mailBody.querySelector(".moz-signature");
+
+ // Are we replying?
+ let replyCitation = mailBody.querySelector(".moz-cite-prefix");
+ if (replyCitation) {
+ if (gCurrentIdentity && gCurrentIdentity.replyOnTop == 0) {
+ // Replying below quote - we'll select the point right before
+ // the signature. If there's no signature, we'll just use the
+ // last node.
+ if (signature && signature.previousSibling) {
+ selection.collapse(
+ mailBody,
+ Array.from(childNodes).indexOf(signature.previousSibling)
+ );
+ } else {
+ selection.collapse(mailBody, childNodes.length - 1);
+ editor.insertLineBreak();
+
+ if (!gMsgCompose.composeHTML) {
+ editor.insertLineBreak();
+ }
+
+ selection.collapse(mailBody, childNodes.length - 2);
+ }
+ } else if (replyCitation.previousSibling) {
+ // Replying above quote
+ let nodeIndex = Array.from(childNodes).indexOf(
+ replyCitation.previousSibling
+ );
+ if (nodeIndex <= 0) {
+ editor.insertLineBreak();
+ nodeIndex = 1;
+ }
+ selection.collapse(mailBody, nodeIndex);
+ } else {
+ editor.beginningOfDocument();
+ editor.insertLineBreak();
+ }
+ return;
+ }
+
+ // Are we forwarding?
+ let forwardBody = mailBody.querySelector(".moz-forward-container");
+ if (forwardBody) {
+ if (forwardBody.previousSibling) {
+ let nodeIndex = Array.from(childNodes).indexOf(
+ forwardBody.previousSibling
+ );
+ if (nodeIndex <= 0) {
+ editor.insertLineBreak();
+ nodeIndex = 1;
+ }
+ // If we're forwarding, insert just before the forward body.
+ selection.collapse(mailBody, nodeIndex);
+ } else {
+ // Just insert after a linebreak at the top.
+ editor.beginningOfDocument();
+ editor.insertLineBreak();
+ selection.collapse(mailBody, 1);
+ }
+ return;
+ }
+
+ // If we haven't figured it out at this point, let's see if there's a
+ // signature, and just insert before it.
+ if (signature && signature.previousSibling) {
+ let nodeIndex = Array.from(childNodes).indexOf(signature.previousSibling);
+ if (nodeIndex <= 0) {
+ editor.insertLineBreak();
+ nodeIndex = 1;
+ }
+ selection.collapse(mailBody, nodeIndex);
+ return;
+ }
+
+ // If we haven't figured it out at this point, let's just put it
+ // at the bottom of the message body. If the "bottom" is also the top,
+ // then we'll insert a linebreak just above it.
+ let nodeIndex = childNodes.length - 1;
+ if (nodeIndex <= 0) {
+ editor.insertLineBreak();
+ nodeIndex = 1;
+ }
+ selection.collapse(mailBody, nodeIndex);
+ },
+
+ /**
+ * Attempts to find any elements with an id in aIDs, and sets those elements
+ * id attribute to the empty string, freeing up the ids for later use.
+ *
+ * @param {Document} aDocument - the document to search for the elements
+ * @param {string[]} aIDs - an array of id strings
+ */
+ _resetNodeIDs(aDocument, aIDs) {
+ for (let id of aIDs) {
+ let node = aDocument.getElementById(id);
+ if (node) {
+ node.id = "";
+ }
+ }
+ },
+
+ /**
+ * Insert the header for the cloud attachment list, which we'll use to
+ * as an insertion point for the individual cloud attachments.
+ *
+ * @param {Document} aDocument - the document to insert the header into
+ */
+ _insertHeader(aDocument) {
+ // If there already exists a cloudAttachmentListRoot,
+ // cloudAttachmentListHeader, cloudAttachmentListFooter or
+ // cloudAttachmentList in the document, strip them of their IDs so that we
+ // don't conflict with them.
+ this._resetNodeIDs(aDocument, [
+ "cloudAttachmentListRoot",
+ "cloudAttachmentListHeader",
+ "cloudAttachmentList",
+ "cloudAttachmentListFooter",
+ ]);
+
+ let editor = GetCurrentEditor();
+ let selection = editor.selection;
+ let originalAnchor = selection.anchorNode;
+ let originalOffset = selection.anchorOffset;
+
+ // Save off the selection ranges so we can restore them later.
+ let ranges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ ranges.push(selection.getRangeAt(i));
+ }
+
+ this._findInsertionPoint(aDocument);
+
+ let root = editor.createElementWithDefaults("div");
+ let header = editor.createElementWithDefaults("div");
+ let list = editor.createElementWithDefaults("div");
+ let footer = editor.createElementWithDefaults("div");
+
+ if (gMsgCompose.composeHTML) {
+ root.style.padding = "15px";
+ root.style.backgroundColor = "#D9EDFF";
+
+ header.style.marginBottom = "15px";
+
+ list = editor.createElementWithDefaults("ul");
+ list.style.backgroundColor = "#FFFFFF";
+ list.style.padding = "15px";
+ list.style.listStyleType = "none";
+ list.display = "inline-block";
+ }
+
+ root.id = "cloudAttachmentListRoot";
+ header.id = "cloudAttachmentListHeader";
+ list.id = "cloudAttachmentList";
+ footer.id = "cloudAttachmentListFooter";
+
+ // It's really quite strange, but if we don't set
+ // the innerHTML of each element to be non-empty, then
+ // the nodes fail to be added to the compose window.
+ root.innerHTML = " ";
+ header.innerHTML = " ";
+ list.innerHTML = " ";
+ footer.innerHTML = " ";
+
+ root.appendChild(header);
+ root.appendChild(list);
+ root.appendChild(footer);
+ editor.insertElementAtSelection(root, false);
+ if (!root.previousSibling || root.previousSibling.localName == "span") {
+ root.parentNode.insertBefore(editor.document.createElement("br"), root);
+ }
+
+ // Remove the space, which would end up in the plain text converted
+ // version.
+ list.innerHTML = "";
+ selection.collapse(originalAnchor, originalOffset);
+
+ // Restore the selection ranges.
+ for (let range of ranges) {
+ selection.addRange(range);
+ }
+ },
+
+ /**
+ * Updates the count of how many attachments have been added
+ * in HTML emails.
+ *
+ * @param {Document} aDocument - the document that contains the header node
+ */
+ async _updateAttachmentCount(aDocument) {
+ let header = aDocument.getElementById("cloudAttachmentListHeader");
+ if (!header) {
+ return;
+ }
+
+ let entries = aDocument.querySelectorAll(
+ "#cloudAttachmentList > .cloudAttachmentItem"
+ );
+
+ header.textContent = await l10nCompose.formatValue(
+ "cloud-file-count-header",
+ {
+ count: entries.length,
+ }
+ );
+ },
+
+ /**
+ * Updates the service provider links in the footer.
+ *
+ * @param {Document} aDocument - the document that contains the footer node
+ */
+ async _updateServiceProviderLinks(aDocument) {
+ let footer = aDocument.getElementById("cloudAttachmentListFooter");
+ if (!footer) {
+ return;
+ }
+
+ let providers = [];
+ let entries = aDocument.querySelectorAll(
+ "#cloudAttachmentList > .cloudAttachmentItem"
+ );
+ for (let entry of entries) {
+ if (!entry.dataset.serviceUrl) {
+ continue;
+ }
+
+ let link_markup = this._generateLink(
+ aDocument,
+ entry.dataset.serviceName,
+ entry.dataset.serviceUrl,
+ "dark-grey"
+ ).outerHTML;
+
+ if (!providers.includes(link_markup)) {
+ providers.push(link_markup);
+ }
+ }
+
+ let content = "";
+ if (providers.length == 1) {
+ content = await l10nCompose.formatValue(
+ "cloud-file-service-provider-footer-single",
+ {
+ link: providers[0],
+ }
+ );
+ } else if (providers.length > 1) {
+ let lastLink = providers.pop();
+ let firstLinks = providers.join(", ");
+ content = await l10nCompose.formatValue(
+ "cloud-file-service-provider-footer-multiple",
+ {
+ firstLinks,
+ lastLink,
+ }
+ );
+ }
+
+ if (gMsgCompose.composeHTML) {
+ // eslint-disable-next-line no-unsanitized/property
+ footer.innerHTML = content;
+ } else {
+ footer.textContent = this._getTextRepresentation(content);
+ }
+ },
+
+ /**
+ * Insert the information for a cloud attachment.
+ *
+ * @param {Document} aDocument - the document to insert the item into
+ * @param {CloudFileTemplate} aCloudFileUpload - object with information about
+ * the uploaded file
+ */
+ async _insertItem(aDocument, aCloudFileUpload) {
+ let list = aDocument.getElementById("cloudAttachmentList");
+
+ if (!list) {
+ this._insertHeader(aDocument);
+ list = aDocument.getElementById("cloudAttachmentList");
+ }
+ list.appendChild(await this._createNode(aDocument, aCloudFileUpload));
+ await this._updateAttachmentCount(aDocument);
+ await this._updateServiceProviderLinks(aDocument);
+ },
+
+ /**
+ * @typedef CloudFileDate
+ * @property {integer} timestamp - milliseconds since epoch
+ * @property {DateTimeFormat} format - format object of Intl.DateTimeFormat
+ */
+
+ /**
+ * @typedef CloudFileTemplate
+ * @property {string} serviceName - name of the upload service provider
+ * @property {string} serviceIcon - icon of the upload service provider
+ * @property {string} serviceUrl - web interface of the upload service provider
+ * @property {boolean} downloadPasswordProtected - link is password protected
+ * @property {integer} downloadLimit - download limit of the link
+ * @property {CloudFileDate} downloadExpiryDate - expiry date of the link
+ */
+
+ /**
+ * Create the link node for a cloud attachment.
+ *
+ * @param {Document} aDocument - the document to insert the item into
+ * @param {CloudFileTemplate} aCloudFileUpload - object with information about
+ * the uploaded file
+ * @param {boolean} composeHTML - override gMsgCompose.composeHTML
+ */
+ async _createNode(
+ aDocument,
+ aCloudFileUpload,
+ composeHTML = gMsgCompose.composeHTML
+ ) {
+ const iconSize = 32;
+ const locales = {
+ service: 0,
+ size: 1,
+ link: 2,
+ "password-protected-link": 3,
+ "expiry-date": 4,
+ "download-limit": 5,
+ "tooltip-password-protected-link": 6,
+ };
+
+ let l10n_values = await l10nCompose.formatValues([
+ { id: "cloud-file-template-service-name" },
+ { id: "cloud-file-template-size" },
+ { id: "cloud-file-template-link" },
+ { id: "cloud-file-template-password-protected-link" },
+ { id: "cloud-file-template-expiry-date" },
+ { id: "cloud-file-template-download-limit" },
+ { id: "cloud-file-tooltip-password-protected-link" },
+ ]);
+
+ let node = aDocument.createElement("li");
+ node.style.border = "1px solid #CDCDCD";
+ node.style.borderRadius = "5px";
+ node.style.marginTop = "10px";
+ node.style.marginBottom = "10px";
+ node.style.padding = "15px";
+ node.style.display = "grid";
+ node.style.gridTemplateColumns = "0fr 1fr 0fr 0fr";
+ node.style.alignItems = "center";
+
+ const statsRow = (name, content, contentLink) => {
+ let entry = aDocument.createElement("span");
+ entry.style.gridColumn = `2 / span 3`;
+ entry.style.fontSize = "small";
+
+ let description = aDocument.createElement("span");
+ description.style.color = "dark-grey";
+ description.textContent = `${l10n_values[locales[name]]} `;
+ entry.appendChild(description);
+
+ let value;
+ if (composeHTML && contentLink) {
+ value = this._generateLink(aDocument, content, contentLink, "#595959");
+ } else {
+ value = aDocument.createElement("span");
+ value.style.color = "#595959";
+ value.textContent = content;
+ }
+ value.classList.add(`cloudfile-${name}`);
+ entry.appendChild(value);
+
+ entry.appendChild(aDocument.createElement("br"));
+ return entry;
+ };
+
+ const serviceRow = () => {
+ let service = aDocument.createDocumentFragment();
+
+ let description = aDocument.createElement("span");
+ description.style.display = "none";
+ description.textContent = `${l10n_values[locales.service]} `;
+ service.appendChild(description);
+
+ let providerName = aDocument.createElement("span");
+ providerName.style.gridArea = "1 / 4";
+ providerName.style.color = "#595959";
+ providerName.style.fontSize = "small";
+ providerName.textContent = aCloudFileUpload.serviceName;
+ providerName.classList.add("cloudfile-service-name");
+ service.appendChild(providerName);
+
+ service.appendChild(aDocument.createElement("br"));
+ return service;
+ };
+
+ // If this message is send in plain text only, do not add a link to the file
+ // name.
+ let name = aDocument.createElement("span");
+ name.textContent = aCloudFileUpload.name;
+ if (composeHTML) {
+ name = this._generateLink(
+ aDocument,
+ aCloudFileUpload.name,
+ aCloudFileUpload.url,
+ "#0F7EDB"
+ );
+ name.setAttribute("moz-do-not-send", "true");
+ name.style.gridArea = "1 / 2";
+ }
+ name.classList.add("cloudfile-name");
+ node.appendChild(name);
+
+ let paperclip = aDocument.createElement("img");
+ paperclip.classList.add("paperClipIcon");
+ paperclip.style.gridArea = "1 / 1";
+ paperclip.alt = "";
+ paperclip.style.marginRight = "5px";
+ paperclip.width = `${iconSize}`;
+ paperclip.height = `${iconSize}`;
+ if (aCloudFileUpload.downloadPasswordProtected) {
+ paperclip.title = l10n_values[locales["tooltip-password-protected-link"]];
+ paperclip.src =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIfSURBVFhH7ZfLK0RRHMfvNd6PMV4Lj5UkO5bslJIdf4ClRw2TlY2yt2EhsZO9DYoFoiSvJBZkI6SsNMyIiLnH93vmXDF5HNe9pHzqM797fufMPb+Zc4Z7jC+QBnvgJryD93AddkH2eUop3IPiHXdgCfSEdLgLOdE+bIFFSl4zZxeRAl2HXzsn2IIZTCTAHPs4hsvhOlxz3rxRtt6GfRyzJlsucw1582zZehv2cUxEtlyGN6afkThuFa7EL7+H0wK03pek4q/xJwtYVv4YumurO+4V/3vgvwAvC5iHTfHL9zFV/Ah7J9tjE9s2r/K3YwWlD8IaREP+ExPCWBDJVl+gM3LEto0nBURHCiuNpBiflvLjqWcufDFfdVbo4ly1PVoC0xrAaz4qnLdiVjk1hVhArvDRFxuSYxQeFSAaGHzCbAuEIsf0URjtsithX3i1Cf18yewKn8kWyOu+OlWXuSpKnBRwpWKxioTXi7BCtr6Ak004BZvhJAwyAUZhb3Q0bwKxXmY+xVzyB8MNOgXwE/NrC0A+clXBDZV7iYkC7GK18AcvTZ0lOFGRE5NDWAtn4A28hdPQEToFcG1Jq4qERXAZ+DCaBXk+cIROAePQgh2whgk30SngAA7CVDgLq6Fr6P4M++Ec5PmPp6BhWAdzIA+m3BOO0C2AJ2GuMyfme0KQp6Ao5EmZf/fLDGFuI2oi+EEcUQm5JDywhpWc2MFGNIwn/WmcKhqF50UAAAAASUVORK5CYII=";
+ } else {
+ paperclip.src =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAVFJREFUWIXtl8FKw0AQhj8EbQ/p0Ut8AVEPgYLUB+i5L6J9E0Wtr1HPgl48WU8K1Tfw4LktxUAhHvZfiMXUbdhVhB0Yms78M/NldwkJuFsD6AMjYCYfASfKBbUd4BkoKvxJmiDWKA1/AXrAtrynmIUIshJ9DXgEmt/km8oVwHEIANu8u0LTleYhBMBUzZMVmkSaSQgAe9DW1d3L/wzAqW6jJpQ3+5cA3vbW1Vz3Np6BCBABIkAE+DWAmX7TUixdynm15Wf6jf5fa3Cq60K5qrraNuHrK1kbmJcGWJ8rB9DC4yvaq5odlmK7wBB4lw8Vs9ZRzdgHwLmaXa5RM1DNmQ+AA2ABfACZgz4DctXs+QAAuMLc0dsPEJk0BXDhazjAFnCnxjlmiTuYg5kAR4rl0twCmz4BLMQAs7RVH6kLzJ17H162fczhGmO+mqa6PqXGnn8CxMN0PcC9DrQAAAAASUVORK5CYII=";
+ }
+ node.appendChild(paperclip);
+
+ let serviceIcon = aDocument.createElement("img");
+ serviceIcon.classList.add("cloudfile-service-icon");
+ serviceIcon.style.gridArea = "1 / 3";
+ serviceIcon.alt = "";
+ serviceIcon.style.margin = "0 5px";
+ serviceIcon.width = `${iconSize}`;
+ serviceIcon.height = `${iconSize}`;
+ node.appendChild(serviceIcon);
+
+ if (aCloudFileUpload.serviceIcon) {
+ if (!/^(chrome|moz-extension):\/\//i.test(aCloudFileUpload.serviceIcon)) {
+ serviceIcon.src = aCloudFileUpload.serviceIcon;
+ } else {
+ try {
+ // Let's use the goodness from MsgComposeCommands.js since we're
+ // sitting right in a compose window.
+ serviceIcon.src = window.loadBlockedImage(
+ aCloudFileUpload.serviceIcon,
+ true
+ );
+ } catch (e) {
+ // Couldn't load the referenced image.
+ console.error(e);
+ }
+ }
+ }
+ node.appendChild(aDocument.createElement("br"));
+
+ node.appendChild(
+ statsRow("size", gMessenger.formatFileSize(aCloudFileUpload.size))
+ );
+
+ if (aCloudFileUpload.downloadExpiryDate) {
+ node.appendChild(
+ statsRow(
+ "expiry-date",
+ new Date(
+ aCloudFileUpload.downloadExpiryDate.timestamp
+ ).toLocaleString(
+ undefined,
+ aCloudFileUpload.downloadExpiryDate.format || {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ timeZoneName: "short",
+ }
+ )
+ )
+ );
+ }
+
+ if (aCloudFileUpload.downloadLimit) {
+ node.appendChild(
+ statsRow("download-limit", aCloudFileUpload.downloadLimit)
+ );
+ }
+
+ if (composeHTML || aCloudFileUpload.serviceUrl) {
+ node.appendChild(serviceRow());
+ }
+
+ let linkElementLocaleId = aCloudFileUpload.downloadPasswordProtected
+ ? "password-protected-link"
+ : "link";
+ node.appendChild(
+ statsRow(linkElementLocaleId, aCloudFileUpload.url, aCloudFileUpload.url)
+ );
+
+ // An extra line break is needed for the converted plain text version, if it
+ // should have a gap between its <li> elements.
+ if (composeHTML) {
+ node.appendChild(aDocument.createElement("br"));
+ }
+
+ // Generate the plain text version from the HTML. The used method needs a <ul>
+ // element wrapped around the <li> element to produce the correct content.
+ if (!composeHTML) {
+ let ul = aDocument.createElement("ul");
+ ul.appendChild(node);
+ node = aDocument.createElement("p");
+ node.textContent = this._getTextRepresentation(ul.outerHTML);
+ }
+
+ node.className = "cloudAttachmentItem";
+ node.dataset.contentLocation = aCloudFileUpload.url;
+ node.dataset.serviceName = aCloudFileUpload.serviceName;
+ node.dataset.serviceUrl = aCloudFileUpload.serviceUrl;
+ return node;
+ },
+
+ /**
+ * Event handler for when mail is sent. For mail that is being sent
+ * (and not saved!), find any cloudAttachmentList* nodes that we've created,
+ * and strip their IDs out. That way, if the receiving user replies by
+ * sending some BigFiles, we don't run into ID conflicts.
+ */
+ send(aEvent) {
+ let msgType = parseInt(aEvent.target.getAttribute("msgtype"));
+
+ if (
+ msgType == Ci.nsIMsgCompDeliverMode.Now ||
+ msgType == Ci.nsIMsgCompDeliverMode.Later ||
+ msgType == Ci.nsIMsgCompDeliverMode.Background
+ ) {
+ const kIDs = [
+ "cloudAttachmentListRoot",
+ "cloudAttachmentListHeader",
+ "cloudAttachmentList",
+ "cloudAttachmentListFooter",
+ ];
+ let mailDoc = document.getElementById("messageEditor").contentDocument;
+
+ for (let id of kIDs) {
+ let element = mailDoc.getElementById(id);
+ if (element) {
+ element.removeAttribute("id");
+ }
+ }
+ }
+ },
+};
+
+window.addEventListener(
+ "compose-window-init",
+ gCloudAttachmentLinkManager.init.bind(gCloudAttachmentLinkManager),
+ true
+);
+window.addEventListener(
+ "compose-send-message",
+ gCloudAttachmentLinkManager.send.bind(gCloudAttachmentLinkManager),
+ true
+);
diff --git a/comm/mail/components/compose/content/dialogs/EdAEAttributes.js b/comm/mail/components/compose/content/dialogs/EdAEAttributes.js
new file mode 100644
index 0000000000..52b7e30fac
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdAEAttributes.js
@@ -0,0 +1,973 @@
+/* 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/. */
+
+// HTML Attributes object for "Name" menulist
+var gHTMLAttr = {};
+
+// JS Events Attributes object for "Name" menulist
+var gJSAttr = {};
+
+// Core HTML attribute values //
+// This is appended to Name menulist when "_core" is attribute name
+var gCoreHTMLAttr = ["^id", "class", "title"];
+
+// Core event attribute values //
+// This is appended to all JS menulists
+// except those elements having "noJSEvents"
+// as a value in their gJSAttr array.
+var gCoreJSEvents = [
+ "onclick",
+ "ondblclick",
+ "onmousedown",
+ "onmouseup",
+ "onmouseover",
+ "onmousemove",
+ "onmouseout",
+ "-",
+ "onkeypress",
+ "onkeydown",
+ "onkeyup",
+];
+
+// Following are commonly-used strings
+
+// Also accept: sRGB: #RRGGBB //
+var gHTMLColors = [
+ "Aqua",
+ "Black",
+ "Blue",
+ "Fuchsia",
+ "Gray",
+ "Green",
+ "Lime",
+ "Maroon",
+ "Navy",
+ "Olive",
+ "Purple",
+ "Red",
+ "Silver",
+ "Teal",
+ "White",
+ "Yellow",
+];
+
+var gHAlign = ["left", "center", "right"];
+
+var gHAlignJustify = ["left", "center", "right", "justify"];
+
+var gHAlignTableContent = ["left", "center", "right", "justify", "char"];
+
+var gVAlignTable = ["top", "middle", "bottom", "baseline"];
+
+var gTarget = ["_blank", "_self", "_parent", "_top"];
+
+// ================ HTML Attributes ================ //
+/* For each element, there is an array of attributes,
+ whose name is the element name,
+ used to fill the "Attribute Name" menulist.
+ For each of those attributes, if they have a specific
+ set of values, those are listed in an array named:
+ "elementName_attName".
+
+ In each values string, the following characters
+ are signal to do input filtering:
+ "#" Allow only integer values
+ "%" Allow integer values or a number ending in "%"
+ "+" Allow integer values and allow "+" or "-" as first character
+ "!" Allow only one character
+ "^" The first character can be only be A-Z, a-z, hyphen, underscore, colon or period
+ "$" is an attribute required by HTML DTD
+*/
+
+/*
+ Most elements have the "dir" attribute,
+ so we use this value array
+ for all elements instead of specifying
+ separately for each element
+*/
+gHTMLAttr.all_dir = ["ltr", "rtl"];
+
+gHTMLAttr.a = [
+ "charset",
+ "type",
+ "name",
+ "href",
+ "^hreflang",
+ "target",
+ "rel",
+ "rev",
+ "!accesskey",
+ "shape", // with imagemap //
+ "coords", // with imagemap //
+ "#tabindex",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.a_target = gTarget;
+
+gHTMLAttr.a_rel = [
+ "alternate",
+ "stylesheet",
+ "start",
+ "next",
+ "prev",
+ "contents",
+ "index",
+ "glossary",
+ "copyright",
+ "chapter",
+ "section",
+ "subsection",
+ "appendix",
+ "help",
+ "bookmark",
+];
+
+gHTMLAttr.a_rev = [
+ "alternate",
+ "stylesheet",
+ "start",
+ "next",
+ "prev",
+ "contents",
+ "index",
+ "glossary",
+ "copyright",
+ "chapter",
+ "section",
+ "subsection",
+ "appendix",
+ "help",
+ "bookmark",
+];
+
+gHTMLAttr.a_shape = ["rect", "circle", "poly", "default"];
+
+gHTMLAttr.abbr = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.acronym = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.address = ["_core", "-", "^lang", "dir"];
+
+// this is deprecated //
+gHTMLAttr.applet = [
+ "codebase",
+ "archive",
+ "code",
+ "object",
+ "alt",
+ "name",
+ "%$width",
+ "%$height",
+ "align",
+ "#hspace",
+ "#vspace",
+ "-",
+ "_core",
+];
+
+gHTMLAttr.applet_align = ["top", "middle", "bottom", "left", "right"];
+
+gHTMLAttr.area = [
+ "shape",
+ "coords",
+ "href",
+ "nohref",
+ "target",
+ "$alt",
+ "#tabindex",
+ "!accesskey",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.area_target = gTarget;
+
+gHTMLAttr.area_shape = ["rect", "circle", "poly", "default"];
+
+gHTMLAttr.area_nohref = ["nohref"];
+
+gHTMLAttr.b = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.base = ["href", "target"];
+
+gHTMLAttr.base_target = gTarget;
+
+// this is deprecated //
+gHTMLAttr.basefont = ["^id", "$size", "color", "face"];
+
+gHTMLAttr.basefont_color = gHTMLColors;
+
+gHTMLAttr.bdo = ["_core", "-", "^lang", "$dir"];
+
+gHTMLAttr.bdo_dir = ["ltr", "rtl"];
+
+gHTMLAttr.big = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.blockquote = ["cite", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.body = [
+ "background",
+ "bgcolor",
+ "text",
+ "link",
+ "vlink",
+ "alink",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.body_bgcolor = gHTMLColors;
+
+gHTMLAttr.body_text = gHTMLColors;
+
+gHTMLAttr.body_link = gHTMLColors;
+
+gHTMLAttr.body_vlink = gHTMLColors;
+
+gHTMLAttr.body_alink = gHTMLColors;
+
+gHTMLAttr.br = ["clear", "-", "_core"];
+
+gHTMLAttr.br_clear = ["none", "left", "all", "right"];
+
+gHTMLAttr.button = [
+ "name",
+ "value",
+ "$type",
+ "disabled",
+ "#tabindex",
+ "!accesskey",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.button_type = ["submit", "button", "reset"];
+
+gHTMLAttr.button_disabled = ["disabled"];
+
+gHTMLAttr.caption = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.caption_align = ["top", "bottom", "left", "right"];
+
+// this is deprecated //
+gHTMLAttr.center = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.cite = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.code = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.col = [
+ "#$span",
+ "%width",
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "char",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.col_span = [
+ "1", // default
+];
+
+gHTMLAttr.col_align = gHAlignTableContent;
+
+gHTMLAttr.col_valign = ["top", "middle", "bottom", "baseline"];
+
+gHTMLAttr.colgroup = [
+ "#$span",
+ "%width",
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.colgroup_span = [
+ "1", // default
+];
+
+gHTMLAttr.colgroup_align = gHAlignTableContent;
+
+gHTMLAttr.colgroup_valign = ["top", "middle", "bottom", "baseline"];
+
+gHTMLAttr.dd = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.del = ["cite", "datetime", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.dfn = ["_core", "-", "^lang", "dir"];
+
+// this is deprecated //
+gHTMLAttr.dir = ["compact", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.dir_compact = ["compact"];
+
+gHTMLAttr.div = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.div_align = gHAlignJustify;
+
+gHTMLAttr.dl = ["compact", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.dl_compact = ["compact"];
+
+gHTMLAttr.dt = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.em = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.fieldset = ["_core", "-", "^lang", "dir"];
+
+// this is deprecated //
+gHTMLAttr.font = ["+size", "color", "face", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.font_color = gHTMLColors;
+
+gHTMLAttr.form = [
+ "$action",
+ "$method",
+ "enctype",
+ "accept",
+ "name",
+ "accept-charset",
+ "target",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.form_method = ["get", "post"];
+
+gHTMLAttr.form_enctype = ["application/x-www-form-urlencoded"];
+
+gHTMLAttr.form_target = gTarget;
+
+gHTMLAttr.frame = [
+ "longdesc",
+ "name",
+ "src",
+ "#frameborder",
+ "#marginwidth",
+ "#marginheight",
+ "noresize",
+ "$scrolling",
+];
+
+gHTMLAttr.frame_frameborder = ["1", "0"];
+
+gHTMLAttr.frame_noresize = ["noresize"];
+
+gHTMLAttr.frame_scrolling = ["auto", "yes", "no"];
+
+gHTMLAttr.frameset = ["rows", "cols", "-", "_core"];
+
+gHTMLAttr.h1 = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.h1_align = gHAlignJustify;
+
+gHTMLAttr.h2 = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.h2_align = gHAlignJustify;
+
+gHTMLAttr.h3 = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.h3_align = gHAlignJustify;
+
+gHTMLAttr.h4 = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.h4_align = gHAlignJustify;
+
+gHTMLAttr.h5 = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.h5_align = gHAlignJustify;
+
+gHTMLAttr.h6 = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.h6_align = gHAlignJustify;
+
+gHTMLAttr.head = ["profile", "-", "^lang", "dir"];
+
+gHTMLAttr.hr = [
+ "align",
+ "noshade",
+ "#size",
+ "%width",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.hr_align = gHAlign;
+
+gHTMLAttr.hr_noshade = ["noshade"];
+
+gHTMLAttr.html = ["version", "-", "^lang", "dir"];
+
+gHTMLAttr.i = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.iframe = [
+ "longdesc",
+ "name",
+ "src",
+ "$frameborder",
+ "marginwidth",
+ "marginheight",
+ "$scrolling",
+ "align",
+ "%height",
+ "%width",
+ "-",
+ "_core",
+];
+
+gHTMLAttr.iframe_frameborder = ["1", "0"];
+
+gHTMLAttr.iframe_scrolling = ["auto", "yes", "no"];
+
+gHTMLAttr.iframe_align = ["top", "middle", "bottom", "left", "right"];
+
+gHTMLAttr.img = [
+ "$src",
+ "$alt",
+ "longdesc",
+ "name",
+ "%height",
+ "%width",
+ "usemap",
+ "ismap",
+ "align",
+ "#border",
+ "#hspace",
+ "#vspace",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.img_ismap = ["ismap"];
+
+gHTMLAttr.img_align = ["top", "middle", "bottom", "left", "right"];
+
+gHTMLAttr.input = [
+ "$type",
+ "name",
+ "value",
+ "checked",
+ "disabled",
+ "readonly",
+ "#size",
+ "#maxlength",
+ "src",
+ "alt",
+ "usemap",
+ "ismap",
+ "#tabindex",
+ "!accesskey",
+ "accept",
+ "align",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.input_type = [
+ "text",
+ "password",
+ "checkbox",
+ "radio",
+ "submit",
+ "reset",
+ "file",
+ "hidden",
+ "image",
+ "button",
+];
+
+gHTMLAttr.input_checked = ["checked"];
+
+gHTMLAttr.input_disabled = ["disabled"];
+
+gHTMLAttr.input_readonly = ["readonly"];
+
+gHTMLAttr.input_ismap = ["ismap"];
+
+gHTMLAttr.input_align = ["top", "middle", "bottom", "left", "right"];
+
+gHTMLAttr.ins = ["cite", "datetime", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.isindex = ["prompt", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.kbd = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.label = ["for", "!accesskey", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.legend = ["!accesskey", "align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.legend_align = ["top", "bottom", "left", "right"];
+
+gHTMLAttr.li = ["type", "#value", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.li_type = ["disc", "square", "circle", "-", "1", "a", "A", "i", "I"];
+
+gHTMLAttr.link = [
+ "charset",
+ "href",
+ "^hreflang",
+ "type",
+ "rel",
+ "rev",
+ "media",
+ "target",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.link_target = gTarget;
+
+gHTMLAttr.link_rel = [
+ "alternate",
+ "stylesheet",
+ "start",
+ "next",
+ "prev",
+ "contents",
+ "index",
+ "glossary",
+ "copyright",
+ "chapter",
+ "section",
+ "subsection",
+ "appendix",
+ "help",
+ "bookmark",
+];
+
+gHTMLAttr.link_rev = [
+ "alternate",
+ "stylesheet",
+ "start",
+ "next",
+ "prev",
+ "contents",
+ "index",
+ "glossary",
+ "copyright",
+ "chapter",
+ "section",
+ "subsection",
+ "appendix",
+ "help",
+ "bookmark",
+];
+
+gHTMLAttr.map = ["$name", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.menu = ["compact", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.menu_compact = ["compact"];
+
+gHTMLAttr.meta = [
+ "http-equiv",
+ "name",
+ "$content",
+ "scheme",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.noframes = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.noscript = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.object = [
+ "declare",
+ "classid",
+ "codebase",
+ "data",
+ "type",
+ "codetype",
+ "archive",
+ "standby",
+ "%height",
+ "%width",
+ "usemap",
+ "name",
+ "#tabindex",
+ "align",
+ "#border",
+ "#hspace",
+ "#vspace",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.object_declare = ["declare"];
+
+gHTMLAttr.object_align = ["top", "middle", "bottom", "left", "right"];
+
+gHTMLAttr.ol = ["type", "compact", "#start", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.ol_type = ["1", "a", "A", "i", "I"];
+
+gHTMLAttr.ol_compact = ["compact"];
+
+gHTMLAttr.optgroup = ["disabled", "$label", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.optgroup_disabled = ["disabled"];
+
+gHTMLAttr.option = [
+ "selected",
+ "disabled",
+ "label",
+ "value",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.option_selected = ["selected"];
+
+gHTMLAttr.option_disabled = ["disabled"];
+
+gHTMLAttr.p = ["align", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.p_align = gHAlignJustify;
+
+gHTMLAttr.param = ["^id", "$name", "value", "$valuetype", "type"];
+
+gHTMLAttr.param_valuetype = ["data", "ref", "object"];
+
+gHTMLAttr.pre = ["%width", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.q = ["cite", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.s = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.samp = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.script = ["charset", "$type", "language", "src", "defer"];
+
+gHTMLAttr.script_defer = ["defer"];
+
+gHTMLAttr.select = [
+ "name",
+ "#size",
+ "multiple",
+ "disabled",
+ "#tabindex",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.select_multiple = ["multiple"];
+
+gHTMLAttr.select_disabled = ["disabled"];
+
+gHTMLAttr.small = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.span = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.strike = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.strong = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.style = ["$type", "media", "title", "-", "^lang", "dir"];
+
+gHTMLAttr.sub = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.sup = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.table = [
+ "summary",
+ "%width",
+ "#border",
+ "frame",
+ "rules",
+ "#cellspacing",
+ "#cellpadding",
+ "align",
+ "bgcolor",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.table_frame = [
+ "void",
+ "above",
+ "below",
+ "hsides",
+ "lhs",
+ "rhs",
+ "vsides",
+ "box",
+ "border",
+];
+
+gHTMLAttr.table_rules = ["none", "groups", "rows", "cols", "all"];
+
+// Note; This is alignment of the table,
+// not table contents, like all other table child elements
+gHTMLAttr.table_align = gHAlign;
+
+gHTMLAttr.table_bgcolor = gHTMLColors;
+
+gHTMLAttr.tbody = [
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.tbody_align = gHAlignTableContent;
+
+gHTMLAttr.tbody_valign = gVAlignTable;
+
+gHTMLAttr.td = [
+ "abbr",
+ "axis",
+ "headers",
+ "scope",
+ "$#rowspan",
+ "$#colspan",
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "nowrap",
+ "bgcolor",
+ "%width",
+ "%height",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.td_scope = ["row", "col", "rowgroup", "colgroup"];
+
+gHTMLAttr.td_rowspan = [
+ "1", // default
+];
+
+gHTMLAttr.td_colspan = [
+ "1", // default
+];
+
+gHTMLAttr.td_align = gHAlignTableContent;
+
+gHTMLAttr.td_valign = gVAlignTable;
+
+gHTMLAttr.td_nowrap = ["nowrap"];
+
+gHTMLAttr.td_bgcolor = gHTMLColors;
+
+gHTMLAttr.textarea = [
+ "name",
+ "$#rows",
+ "$#cols",
+ "disabled",
+ "readonly",
+ "#tabindex",
+ "!accesskey",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.textarea_disabled = ["disabled"];
+
+gHTMLAttr.textarea_readonly = ["readonly"];
+
+gHTMLAttr.tfoot = [
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.tfoot_align = gHAlignTableContent;
+
+gHTMLAttr.tfoot_valign = gVAlignTable;
+
+gHTMLAttr.th = [
+ "abbr",
+ "axis",
+ "headers",
+ "scope",
+ "$#rowspan",
+ "$#colspan",
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "nowrap",
+ "bgcolor",
+ "%width",
+ "%height",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.th_scope = ["row", "col", "rowgroup", "colgroup"];
+
+gHTMLAttr.th_rowspan = [
+ "1", // default
+];
+
+gHTMLAttr.th_colspan = [
+ "1", // default
+];
+
+gHTMLAttr.th_align = gHAlignTableContent;
+
+gHTMLAttr.th_valign = gVAlignTable;
+
+gHTMLAttr.th_nowrap = ["nowrap"];
+
+gHTMLAttr.th_bgcolor = gHTMLColors;
+
+gHTMLAttr.thead = [
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.thead_align = gHAlignTableContent;
+
+gHTMLAttr.thead_valign = gVAlignTable;
+
+gHTMLAttr.title = ["^lang", "dir"];
+
+gHTMLAttr.tr = [
+ "align",
+ "!char",
+ "#charoff",
+ "valign",
+ "bgcolor",
+ "-",
+ "_core",
+ "-",
+ "^lang",
+ "dir",
+];
+
+gHTMLAttr.tr_align = gHAlignTableContent;
+
+gHTMLAttr.tr_valign = gVAlignTable;
+
+gHTMLAttr.tr_bgcolor = gHTMLColors;
+
+gHTMLAttr.tt = ["_core", "-", "^lang", "dir"];
+
+gHTMLAttr.u = ["_core", "-", "^lang", "dir"];
+gHTMLAttr.ul = ["type", "compact", "-", "_core", "-", "^lang", "dir"];
+
+gHTMLAttr.ul_type = ["disc", "square", "circle"];
+
+gHTMLAttr.ul_compact = ["compact"];
+
+// Prefix with "_" since this is reserved (it's stripped out)
+gHTMLAttr._var = ["_core", "-", "^lang", "dir"];
+
+// ================ JS Attributes ================ //
+// These are element specific even handlers.
+/* Most all elements use gCoreJSEvents, so those
+ are assumed except for those listed here with "noEvents"
+*/
+
+gJSAttr.a = ["onfocus", "onblur"];
+
+gJSAttr.area = ["onfocus", "onblur"];
+
+gJSAttr.body = ["onload", "onupload"];
+
+gJSAttr.button = ["onfocus", "onblur"];
+
+gJSAttr.form = ["onsubmit", "onreset"];
+
+gJSAttr.frameset = ["onload", "onunload"];
+
+gJSAttr.input = ["onfocus", "onblur", "onselect", "onchange"];
+
+gJSAttr.label = ["onfocus", "onblur"];
+
+gJSAttr.select = ["onfocus", "onblur", "onchange"];
+
+gJSAttr.textarea = ["onfocus", "onblur", "onselect", "onchange"];
+
+// Elements that don't have JSEvents:
+gJSAttr.font = ["noJSEvents"];
+
+gJSAttr.applet = ["noJSEvents"];
+
+gJSAttr.isindex = ["noJSEvents"];
+
+gJSAttr.iframe = ["noJSEvents"];
diff --git a/comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js b/comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js
new file mode 100644
index 0000000000..ca54fa16da
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js
@@ -0,0 +1,146 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdAdvancedEdit.js */
+/* import-globals-from EdDialogCommon.js */
+
+// build attribute list in tree form from element attributes
+function BuildCSSAttributeTable() {
+ var style = gElement.style;
+ if (style == undefined) {
+ dump("Inline styles undefined\n");
+ return;
+ }
+
+ var declLength = style.length;
+
+ if (declLength == undefined || declLength == 0) {
+ if (declLength == undefined) {
+ dump("Failed to query the number of inline style declarations\n");
+ }
+
+ return;
+ }
+
+ if (declLength > 0) {
+ for (var i = 0; i < declLength; ++i) {
+ var name = style.item(i);
+ var value = style.getPropertyValue(name);
+ AddTreeItem(name, value, "CSSAList", CSSAttrs);
+ }
+ }
+
+ ClearCSSInputWidgets();
+}
+
+function onChangeCSSAttribute() {
+ var name = TrimString(gDialog.AddCSSAttributeNameInput.value);
+ if (!name) {
+ return;
+ }
+
+ var value = TrimString(gDialog.AddCSSAttributeValueInput.value);
+
+ // First try to update existing attribute
+ // If not found, add new attribute
+ if (!UpdateExistingAttribute(name, value, "CSSAList") && value) {
+ AddTreeItem(name, value, "CSSAList", CSSAttrs);
+ }
+}
+
+function ClearCSSInputWidgets() {
+ gDialog.AddCSSAttributeTree.view.selection.clearSelection();
+ gDialog.AddCSSAttributeNameInput.value = "";
+ gDialog.AddCSSAttributeValueInput.value = "";
+ SetTextboxFocus(gDialog.AddCSSAttributeNameInput);
+}
+
+function onSelectCSSTreeItem() {
+ if (!gDoOnSelectTree) {
+ return;
+ }
+
+ var tree = gDialog.AddCSSAttributeTree;
+ if (tree && tree.view.selection.count) {
+ gDialog.AddCSSAttributeNameInput.value = GetTreeItemAttributeStr(
+ getSelectedItem(tree)
+ );
+ gDialog.AddCSSAttributeValueInput.value = GetTreeItemValueStr(
+ getSelectedItem(tree)
+ );
+ }
+}
+
+function onInputCSSAttributeName() {
+ var attName = TrimString(
+ gDialog.AddCSSAttributeNameInput.value
+ ).toLowerCase();
+ var newValue = "";
+
+ var existingValue = GetAndSelectExistingAttributeValue(attName, "CSSAList");
+ if (existingValue) {
+ newValue = existingValue;
+ }
+
+ gDialog.AddCSSAttributeValueInput.value = newValue;
+}
+
+function editCSSAttributeValue(targetCell) {
+ if (IsNotTreeHeader(targetCell)) {
+ gDialog.AddCSSAttributeValueInput.select();
+ }
+}
+
+function UpdateCSSAttributes() {
+ var CSSAList = document.getElementById("CSSAList");
+ var styleString = "";
+ for (var i = 0; i < CSSAList.children.length; i++) {
+ var item = CSSAList.children[i];
+ var name = GetTreeItemAttributeStr(item);
+ var value = GetTreeItemValueStr(item);
+ // this code allows users to be sloppy in typing in values, and enter
+ // things like "foo: " and "bar;". This will trim off everything after the
+ // respective character.
+ if (name.includes(":")) {
+ name = name.substring(0, name.lastIndexOf(":"));
+ }
+ if (value.includes(";")) {
+ value = value.substring(0, value.lastIndexOf(";"));
+ }
+ if (i == CSSAList.children.length - 1) {
+ // Last property.
+ styleString += name + ": " + value + ";";
+ } else {
+ styleString += name + ": " + value + "; ";
+ }
+ }
+ if (styleString) {
+ // Use editor transactions if modifying the element directly in the document
+ doRemoveAttribute("style");
+ doSetAttribute("style", styleString); // NOTE BUG 18894!!!
+ } else if (gElement.getAttribute("style")) {
+ doRemoveAttribute("style");
+ }
+}
+
+function RemoveCSSAttribute() {
+ // We only allow 1 selected item
+ if (gDialog.AddCSSAttributeTree.view.selection.count) {
+ // Remove the item from the tree
+ // We always rebuild complete "style" string,
+ // so no list of "removed" items
+ getSelectedItem(gDialog.AddCSSAttributeTree).remove();
+
+ ClearCSSInputWidgets();
+ }
+}
+
+function SelectCSSTree(index) {
+ gDoOnSelectTree = false;
+ try {
+ gDialog.AddCSSAttributeTree.selectedIndex = index;
+ } catch (e) {}
+ gDoOnSelectTree = true;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js b/comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js
new file mode 100644
index 0000000000..127bfb858b
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js
@@ -0,0 +1,362 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdAdvancedEdit.js */
+/* import-globals-from EdDialogCommon.js */
+
+function BuildHTMLAttributeNameList() {
+ gDialog.AddHTMLAttributeNameInput.removeAllItems();
+
+ var elementName = gElement.localName;
+ var attNames = gHTMLAttr[elementName];
+
+ if (attNames && attNames.length) {
+ var menuitem;
+
+ for (var i = 0; i < attNames.length; i++) {
+ var name = attNames[i];
+
+ if (name == "_core") {
+ // Signal to append the common 'core' attributes.
+ for (var j = 0; j < gCoreHTMLAttr.length; j++) {
+ name = gCoreHTMLAttr[j];
+
+ // only filtering rule used for core attributes as of 8-20-01
+ // Add more rules if necessary.
+ if (name.includes("^")) {
+ name = name.replace(/\^/g, "");
+ menuitem = gDialog.AddHTMLAttributeNameInput.appendItem(name, name);
+ menuitem.setAttribute("limitFirstChar", "true");
+ } else {
+ gDialog.AddHTMLAttributeNameInput.appendItem(name, name);
+ }
+ }
+ } else if (name == "-") {
+ // Signal for separator
+ var popup = gDialog.AddHTMLAttributeNameInput.menupopup;
+ if (popup) {
+ var sep = document.createXULElement("menuseparator");
+ if (sep) {
+ popup.appendChild(sep);
+ }
+ }
+ } else {
+ // Get information about value filtering
+ let forceOneChar = name.includes("!");
+ let forceInteger = name.includes("#");
+ let forceSignedInteger = name.includes("+");
+ let forceIntOrPercent = name.includes("%");
+ let limitFirstChar = name.includes("^");
+ // let required = name.includes("$");
+
+ // Strip flag characters
+ name = name.replace(/[!^#%$+]/g, "");
+
+ menuitem = gDialog.AddHTMLAttributeNameInput.appendItem(name, name);
+ if (menuitem) {
+ // Signify "required" attributes by special style
+ // TODO: Don't do this until next version, when we add
+ // explanatory text and an 'Autofill Required Attributes' button
+ // if (required)
+ // menuitem.setAttribute("class", "menuitem-highlight-1");
+
+ // Set flags to filter value input
+ if (forceOneChar) {
+ menuitem.setAttribute("forceOneChar", "true");
+ }
+ if (limitFirstChar) {
+ menuitem.setAttribute("limitFirstChar", "true");
+ }
+ if (forceInteger) {
+ menuitem.setAttribute("forceInteger", "true");
+ }
+ if (forceSignedInteger) {
+ menuitem.setAttribute("forceSignedInteger", "true");
+ }
+ if (forceIntOrPercent) {
+ menuitem.setAttribute("forceIntOrPercent", "true");
+ }
+ }
+ }
+ }
+ }
+}
+
+// build attribute list in tree form from element attributes
+function BuildHTMLAttributeTable() {
+ var nodeMap = gElement.attributes;
+ var i;
+ if (nodeMap.length > 0) {
+ var added = false;
+ for (i = 0; i < nodeMap.length; i++) {
+ let name = nodeMap[i].name.trim().toLowerCase();
+ if (
+ CheckAttributeNameSimilarity(nodeMap[i].nodeName, HTMLAttrs) ||
+ name.startsWith("on") ||
+ name == "style"
+ ) {
+ continue; // repeated or non-HTML attribute, ignore this one and go to next
+ }
+ if (
+ !name.startsWith("_moz") &&
+ AddTreeItem(name, nodeMap[i].value, "HTMLAList", HTMLAttrs)
+ ) {
+ added = true;
+ }
+ }
+
+ if (added) {
+ SelectHTMLTree(0);
+ }
+ }
+}
+
+function ClearHTMLInputWidgets() {
+ gDialog.AddHTMLAttributeTree.view.selection.clearSelection();
+ gDialog.AddHTMLAttributeNameInput.value = "";
+ gDialog.AddHTMLAttributeValueInput.value = "";
+ SetTextboxFocus(gDialog.AddHTMLAttributeNameInput);
+}
+
+function onSelectHTMLTreeItem() {
+ if (!gDoOnSelectTree) {
+ return;
+ }
+
+ var tree = gDialog.AddHTMLAttributeTree;
+ if (tree && tree.view.selection.count) {
+ var inputName = TrimString(
+ gDialog.AddHTMLAttributeNameInput.value
+ ).toLowerCase();
+ var selectedItem = getSelectedItem(tree);
+ var selectedName =
+ selectedItem.firstElementChild.firstElementChild.getAttribute("label");
+
+ if (inputName == selectedName) {
+ // Already editing selected name - just update the value input
+ gDialog.AddHTMLAttributeValueInput.value =
+ GetTreeItemValueStr(selectedItem);
+ } else {
+ gDialog.AddHTMLAttributeNameInput.value = selectedName;
+
+ // Change value input based on new selected name
+ onInputHTMLAttributeName();
+ }
+ }
+}
+
+function onInputHTMLAttributeName() {
+ let attName = gDialog.AddHTMLAttributeNameInput.value.toLowerCase().trim();
+
+ // Clear value widget, but prevent triggering update in tree
+ gUpdateTreeValue = false;
+ gDialog.AddHTMLAttributeValueInput.value = "";
+ gUpdateTreeValue = true;
+
+ if (attName) {
+ // Get value list for current attribute name
+ var valueListName;
+
+ // Most elements have the "dir" attribute,
+ // so we have just one array for the allowed values instead
+ // requiring duplicate entries for each element in EdAEAttributes.js
+ if (attName == "dir") {
+ valueListName = "all_dir";
+ } else {
+ valueListName = gElement.localName + "_" + attName;
+ }
+
+ // Strip off leading "_" we sometimes use (when element name is reserved word)
+ if (valueListName.startsWith("_")) {
+ valueListName = valueListName.slice(1);
+ }
+
+ let useMenulist = false; // Editable menulist vs. input for the value.
+ var newValue = "";
+ if (valueListName in gHTMLAttr) {
+ var valueList = gHTMLAttr[valueListName];
+
+ let listLen = valueList.length;
+ useMenulist = listLen > 1;
+ if (listLen == 1) {
+ newValue = valueList[0];
+ }
+
+ // Note: For case where "value list" is actually just
+ // one (default) item, don't use menulist for that
+ if (useMenulist) {
+ gDialog.AddHTMLAttributeValueMenulist.removeAllItems();
+
+ // Rebuild the list
+ for (var i = 0; i < listLen; i++) {
+ if (valueList[i] == "-") {
+ // Signal for separator
+ var popup = gDialog.AddHTMLAttributeValueInput.menupopup;
+ if (popup) {
+ var sep = document.createXULElement("menuseparator");
+ if (sep) {
+ popup.appendChild(sep);
+ }
+ }
+ } else {
+ gDialog.AddHTMLAttributeValueMenulist.appendItem(
+ valueList[i],
+ valueList[i]
+ );
+ }
+ }
+ }
+ }
+ if (useMenulist) {
+ // Switch to using editable menulist instead of the input.
+ gDialog.AddHTMLAttributeValueMenulist.parentElement.collapsed = false;
+ gDialog.AddHTMLAttributeValueTextbox.parentElement.collapsed = true;
+ gDialog.AddHTMLAttributeValueInput =
+ gDialog.AddHTMLAttributeValueMenulist;
+ } else {
+ // No list: Use input instead of editable menulist.
+ gDialog.AddHTMLAttributeValueMenulist.parentElement.collapsed = true;
+ gDialog.AddHTMLAttributeValueTextbox.parentElement.collapsed = false;
+ gDialog.AddHTMLAttributeValueInput = gDialog.AddHTMLAttributeValueTextbox;
+ }
+
+ // If attribute already exists in tree, use associated value,
+ // else use default found above
+ var existingValue = GetAndSelectExistingAttributeValue(
+ attName,
+ "HTMLAList"
+ );
+ if (existingValue) {
+ newValue = existingValue;
+ }
+
+ gDialog.AddHTMLAttributeValueInput.value = newValue;
+
+ if (!existingValue) {
+ onInputHTMLAttributeValue();
+ }
+ }
+}
+
+function onInputHTMLAttributeValue() {
+ if (!gUpdateTreeValue) {
+ return;
+ }
+
+ var name = TrimString(gDialog.AddHTMLAttributeNameInput.value);
+ if (!name) {
+ return;
+ }
+
+ // Trim spaces only from left since we must allow spaces within the string
+ // (we always reset the input field's value below)
+ var value = TrimStringLeft(gDialog.AddHTMLAttributeValueInput.value);
+ if (value) {
+ // Do value filtering based on type of attribute
+ // (Do not use "forceInteger()" to avoid multiple
+ // resetting of input's value and flickering)
+ var selectedItem = gDialog.AddHTMLAttributeNameInput.selectedItem;
+
+ if (selectedItem) {
+ if (
+ selectedItem.getAttribute("forceOneChar") == "true" &&
+ value.length > 1
+ ) {
+ value = value.slice(0, 1);
+ }
+
+ if (selectedItem.getAttribute("forceIntOrPercent") == "true") {
+ // Allow integer with optional "%" as last character
+ var percent = TrimStringRight(value).slice(-1);
+ value = value.replace(/\D+/g, "");
+ if (percent == "%") {
+ value += percent;
+ }
+ } else if (selectedItem.getAttribute("forceInteger") == "true") {
+ value = value.replace(/\D+/g, "");
+ } else if (selectedItem.getAttribute("forceSignedInteger") == "true") {
+ // Allow integer with optional "+" or "-" as first character
+ var sign = value[0];
+ value = value.replace(/\D+/g, "");
+ if (sign == "+" || sign == "-") {
+ value = sign + value;
+ }
+ }
+
+ // Special case attributes
+ if (selectedItem.getAttribute("limitFirstChar") == "true") {
+ // Limit first character to letter, and all others to
+ // letters, numbers, and a few others
+ value = value
+ .replace(/^[^a-zA-Z\u0080-\uFFFF]/, "")
+ .replace(/[^a-zA-Z0-9_\.\-\:\u0080-\uFFFF]+/g, "");
+ }
+
+ // Update once only if it changed
+ if (value != gDialog.AddHTMLAttributeValueInput.value) {
+ gDialog.AddHTMLAttributeValueInput.value = value;
+ }
+ }
+ }
+
+ // Update value in the tree list
+ // If not found, add new attribute
+ if (!UpdateExistingAttribute(name, value, "HTMLAList") && value) {
+ AddTreeItem(name, value, "HTMLAList", HTMLAttrs);
+ }
+}
+
+function editHTMLAttributeValue(targetCell) {
+ if (IsNotTreeHeader(targetCell)) {
+ gDialog.AddHTMLAttributeValueInput.select();
+ }
+}
+
+// update the object with added and removed attributes
+function UpdateHTMLAttributes() {
+ var HTMLAList = document.getElementById("HTMLAList");
+ var i;
+
+ // remove removed attributes
+ for (i = 0; i < HTMLRAttrs.length; i++) {
+ var name = HTMLRAttrs[i];
+
+ if (gElement.hasAttribute(name)) {
+ doRemoveAttribute(name);
+ }
+ }
+
+ // Set added or changed attributes
+ for (i = 0; i < HTMLAList.children.length; i++) {
+ var item = HTMLAList.children[i];
+ doSetAttribute(GetTreeItemAttributeStr(item), GetTreeItemValueStr(item));
+ }
+}
+
+function RemoveHTMLAttribute() {
+ // We only allow 1 selected item
+ if (gDialog.AddHTMLAttributeTree.view.selection.count) {
+ var item = getSelectedItem(gDialog.AddHTMLAttributeTree);
+ var attr = GetTreeItemAttributeStr(item);
+
+ // remove the item from the attribute array
+ HTMLRAttrs[HTMLRAttrs.length] = attr;
+ RemoveNameFromAttArray(attr, HTMLAttrs);
+
+ // Remove the item from the tree
+ item.remove();
+
+ // Clear inputs and selected item in tree
+ ClearHTMLInputWidgets();
+ }
+}
+
+function SelectHTMLTree(index) {
+ gDoOnSelectTree = false;
+ try {
+ gDialog.AddHTMLAttributeTree.selectedIndex = index;
+ } catch (e) {}
+ gDoOnSelectTree = true;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js b/comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js
new file mode 100644
index 0000000000..8f902b74cd
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js
@@ -0,0 +1,200 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdAdvancedEdit.js */
+/* import-globals-from EdDialogCommon.js */
+
+function BuildJSEAttributeNameList() {
+ gDialog.AddJSEAttributeNameList.removeAllItems();
+
+ // Get events specific to current element
+ var elementName = gElement.localName;
+ if (elementName in gJSAttr) {
+ var attNames = gJSAttr[elementName];
+ var i;
+ var popup;
+ var sep;
+
+ if (attNames && attNames.length) {
+ // Since we don't allow user-editable JS events yet (but we will soon)
+ // simply remove the JS tab to not allow adding JS events
+ if (attNames[0] == "noJSEvents") {
+ var tab = document.getElementById("tabJSE");
+ if (tab) {
+ tab.remove();
+ }
+
+ return;
+ }
+
+ for (i = 0; i < attNames.length; i++) {
+ gDialog.AddJSEAttributeNameList.appendItem(attNames[i], attNames[i]);
+ }
+
+ popup = gDialog.AddJSEAttributeNameList.firstElementChild;
+ if (popup) {
+ sep = document.createXULElement("menuseparator");
+ if (sep) {
+ popup.appendChild(sep);
+ }
+ }
+ }
+ }
+
+ // Always add core JS events unless we aborted above
+ for (i = 0; i < gCoreJSEvents.length; i++) {
+ if (gCoreJSEvents[i] == "-") {
+ if (!popup) {
+ popup = gDialog.AddJSEAttributeNameList.firstElementChild;
+ }
+
+ sep = document.createXULElement("menuseparator");
+
+ if (popup && sep) {
+ popup.appendChild(sep);
+ }
+ } else {
+ gDialog.AddJSEAttributeNameList.appendItem(
+ gCoreJSEvents[i],
+ gCoreJSEvents[i]
+ );
+ }
+ }
+
+ gDialog.AddJSEAttributeNameList.selectedIndex = 0;
+
+ // Use current name and value of first tree item if it exists
+ onSelectJSETreeItem();
+}
+
+// build attribute list in tree form from element attributes
+function BuildJSEAttributeTable() {
+ var nodeMap = gElement.attributes;
+ if (nodeMap.length > 0) {
+ var added = false;
+ for (var i = 0; i < nodeMap.length; i++) {
+ let name = nodeMap[i].nodeName.toLowerCase();
+ if (CheckAttributeNameSimilarity(nodeMap[i].nodeName, JSEAttrs)) {
+ // Repeated or non-JS handler, ignore this one and go to next.
+ continue;
+ }
+ if (!name.startsWith("on")) {
+ // Attribute isn't an event handler.
+ continue;
+ }
+ var value = gElement.getAttribute(nodeMap[i].nodeName);
+ if (AddTreeItem(name, value, "JSEAList", JSEAttrs)) {
+ // add item to tree
+ added = true;
+ }
+ }
+
+ // Select first item
+ if (added) {
+ gDialog.AddJSEAttributeTree.selectedIndex = 0;
+ }
+ }
+}
+
+function onSelectJSEAttribute() {
+ if (!gDoOnSelectTree) {
+ return;
+ }
+
+ gDialog.AddJSEAttributeValueInput.value = GetAndSelectExistingAttributeValue(
+ gDialog.AddJSEAttributeNameList.label,
+ "JSEAList"
+ );
+}
+
+function onSelectJSETreeItem() {
+ var tree = gDialog.AddJSEAttributeTree;
+ if (tree && tree.view.selection.count) {
+ // Select attribute name in list
+ gDialog.AddJSEAttributeNameList.value = GetTreeItemAttributeStr(
+ getSelectedItem(tree)
+ );
+
+ // Set value input to that in tree (no need to update this in the tree)
+ gUpdateTreeValue = false;
+ gDialog.AddJSEAttributeValueInput.value = GetTreeItemValueStr(
+ getSelectedItem(tree)
+ );
+ gUpdateTreeValue = true;
+ }
+}
+
+function onInputJSEAttributeValue() {
+ if (gUpdateTreeValue) {
+ var name = TrimString(gDialog.AddJSEAttributeNameList.label);
+ var value = TrimString(gDialog.AddJSEAttributeValueInput.value);
+
+ // Update value in the tree list
+ // Since we have a non-editable menulist,
+ // we MUST automatically add the event attribute if it doesn't exist
+ if (!UpdateExistingAttribute(name, value, "JSEAList") && value) {
+ AddTreeItem(name, value, "JSEAList", JSEAttrs);
+ }
+ }
+}
+
+function editJSEAttributeValue(targetCell) {
+ if (IsNotTreeHeader(targetCell)) {
+ gDialog.AddJSEAttributeValueInput.select();
+ }
+}
+
+function UpdateJSEAttributes() {
+ var JSEAList = document.getElementById("JSEAList");
+ var i;
+
+ // remove removed attributes
+ for (i = 0; i < JSERAttrs.length; i++) {
+ var name = JSERAttrs[i];
+
+ if (gElement.hasAttribute(name)) {
+ doRemoveAttribute(name);
+ }
+ }
+
+ // Add events
+ for (i = 0; i < JSEAList.children.length; i++) {
+ var item = JSEAList.children[i];
+
+ // set the event handler
+ doSetAttribute(GetTreeItemAttributeStr(item), GetTreeItemValueStr(item));
+ }
+}
+
+function RemoveJSEAttribute() {
+ // This differs from HTML and CSS panels:
+ // We reselect after removing, because there is not
+ // editable attribute name input, so we can't clear that
+ // like we do in other panels
+ var newIndex = gDialog.AddJSEAttributeTree.selectedIndex;
+
+ // We only allow 1 selected item
+ if (gDialog.AddJSEAttributeTree.view.selection.count) {
+ var item = getSelectedItem(gDialog.AddJSEAttributeTree);
+
+ // Name is the text of the treecell
+ var attr = GetTreeItemAttributeStr(item);
+
+ // remove the item from the attribute array
+ if (newIndex >= JSEAttrs.length - 1) {
+ newIndex--;
+ }
+
+ // remove the item from the attribute array
+ JSERAttrs[JSERAttrs.length] = attr;
+ RemoveNameFromAttArray(attr, JSEAttrs);
+
+ // Remove the item from the tree
+ item.remove();
+
+ // Reselect an item
+ gDialog.AddJSEAttributeTree.selectedIndex = newIndex;
+ }
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js
new file mode 100644
index 0000000000..5f2515c2f6
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js
@@ -0,0 +1,342 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdAEAttributes.js */
+/* import-globals-from EdAECSSAttributes.js */
+/* import-globals-from EdAEHTMLAttributes.js */
+/* import-globals-from EdAEJSEAttributes.js */
+/* import-globals-from EdDialogCommon.js */
+
+/** ************ GLOBALS */
+var gElement = null; // handle to actual element edited
+
+var HTMLAttrs = []; // html attributes
+var CSSAttrs = []; // css attributes
+var JSEAttrs = []; // js events
+
+var HTMLRAttrs = []; // removed html attributes
+var JSERAttrs = []; // removed js events
+
+/* Set false to allow changing selection in tree
+ without doing "onselect" handler actions
+*/
+var gDoOnSelectTree = true;
+var gUpdateTreeValue = true;
+
+/** ************ INITIALISATION && SETUP */
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+/**
+ * function : void Startup();
+ * parameters : none
+ * returns : none
+ * desc. : startup and initialisation, prepares dialog.
+ */
+function Startup() {
+ var editor = GetCurrentEditor();
+
+ // Element to edit is passed in
+ if (!editor || !window.arguments[1]) {
+ dump("Advanced Edit: No editor or element to edit not supplied\n");
+ window.close();
+ return;
+ }
+ // This is the return value for the parent,
+ // who only needs to know if OK was clicked
+ window.opener.AdvancedEditOK = false;
+
+ // The actual element edited (not a copy!)
+ gElement = window.arguments[1];
+
+ // place the tag name in the header
+ var tagLabel = document.getElementById("tagLabel");
+ tagLabel.setAttribute("value", "<" + gElement.localName + ">");
+
+ // Create dialog object to store controls for easy access
+ gDialog.AddHTMLAttributeNameInput = document.getElementById(
+ "AddHTMLAttributeNameInput"
+ );
+
+ gDialog.AddHTMLAttributeValueMenulist = document.getElementById(
+ "AddHTMLAttributeValueMenulist"
+ );
+ gDialog.AddHTMLAttributeValueTextbox = document.getElementById(
+ "AddHTMLAttributeValueTextbox"
+ );
+ gDialog.AddHTMLAttributeValueInput = gDialog.AddHTMLAttributeValueTextbox;
+
+ gDialog.AddHTMLAttributeTree = document.getElementById("HTMLATree");
+ gDialog.AddCSSAttributeNameInput = document.getElementById(
+ "AddCSSAttributeNameInput"
+ );
+ gDialog.AddCSSAttributeValueInput = document.getElementById(
+ "AddCSSAttributeValueInput"
+ );
+ gDialog.AddCSSAttributeTree = document.getElementById("CSSATree");
+ gDialog.AddJSEAttributeNameList = document.getElementById(
+ "AddJSEAttributeNameList"
+ );
+ gDialog.AddJSEAttributeValueInput = document.getElementById(
+ "AddJSEAttributeValueInput"
+ );
+ gDialog.AddJSEAttributeTree = document.getElementById("JSEATree");
+ gDialog.okButton = document.querySelector("dialog").getButton("accept");
+
+ // build the attribute trees
+ BuildHTMLAttributeTable();
+ BuildCSSAttributeTable();
+ BuildJSEAttributeTable();
+
+ // Build attribute name arrays for menulists
+ BuildJSEAttributeNameList();
+ BuildHTMLAttributeNameList();
+ // No menulists for CSS panel (yet)
+
+ // Set focus to Name editable menulist in HTML panel
+ SetTextboxFocus(gDialog.AddHTMLAttributeNameInput);
+
+ // size the dialog properly
+ window.sizeToContent();
+
+ SetWindowLocation();
+}
+
+/**
+ * function : bool onAccept ( void );
+ * parameters : none
+ * returns : boolean true to close the window
+ * desc. : event handler for ok button
+ */
+function onAccept() {
+ var editor = GetCurrentEditor();
+ editor.beginTransaction();
+ try {
+ // Update our gElement attributes
+ UpdateHTMLAttributes();
+ UpdateCSSAttributes();
+ UpdateJSEAttributes();
+ } catch (ex) {
+ dump(ex);
+ }
+ editor.endTransaction();
+
+ window.opener.AdvancedEditOK = true;
+ SaveWindowLocation();
+}
+
+// Helpers for removing and setting attributes
+// Use editor transactions if modifying the element already in the document
+// (Temporary element from a property dialog won't have a parent node)
+function doRemoveAttribute(attrib) {
+ try {
+ var editor = GetCurrentEditor();
+ if (gElement.parentNode) {
+ editor.removeAttribute(gElement, attrib);
+ } else {
+ gElement.removeAttribute(attrib);
+ }
+ } catch (ex) {}
+}
+
+function doSetAttribute(attrib, value) {
+ try {
+ var editor = GetCurrentEditor();
+ if (gElement.parentNode) {
+ editor.setAttribute(gElement, attrib, value);
+ } else {
+ gElement.setAttribute(attrib, value);
+ }
+ } catch (ex) {}
+}
+
+/**
+ * function : bool CheckAttributeNameSimilarity ( string attName, array attArray );
+ * parameters : attribute to look for, array of current attributes
+ * returns : true if attribute already exists, false if it does not
+ * desc. : checks to see if any other attributes by the same name as the arg supplied
+ * already exist.
+ */
+function CheckAttributeNameSimilarity(attName, attArray) {
+ for (var i = 0; i < attArray.length; i++) {
+ if (attName.toLowerCase() == attArray[i].toLowerCase()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * function : bool UpdateExistingAttribute ( string attName, string attValue, string treeChildrenId );
+ * parameters : attribute to look for, new value, ID of <treeChildren> node in XUL tree
+ * returns : true if attribute already exists in tree, false if it does not
+ * desc. : checks to see if any other attributes by the same name as the arg supplied
+ * already exist while setting the associated value if different from current value
+ */
+function UpdateExistingAttribute(attName, attValue, treeChildrenId) {
+ var treeChildren = document.getElementById(treeChildrenId);
+ if (!treeChildren) {
+ return false;
+ }
+
+ var name;
+ var i;
+ attName = TrimString(attName).toLowerCase();
+ attValue = TrimString(attValue);
+
+ for (i = 0; i < treeChildren.children.length; i++) {
+ var item = treeChildren.children[i];
+ name = GetTreeItemAttributeStr(item);
+ if (name.toLowerCase() == attName) {
+ // Set the text in the "value' column treecell
+ SetTreeItemValueStr(item, attValue);
+
+ // Select item just changed,
+ // but don't trigger the tree's onSelect handler
+ gDoOnSelectTree = false;
+ try {
+ selectTreeItem(treeChildren, item);
+ } catch (e) {}
+ gDoOnSelectTree = true;
+
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * function : string GetAndSelectExistingAttributeValue ( string attName, string treeChildrenId );
+ * parameters : attribute to look for, ID of <treeChildren> node in XUL tree
+ * returns : value in from the tree or empty string if name not found
+ */
+function GetAndSelectExistingAttributeValue(attName, treeChildrenId) {
+ if (!attName) {
+ return "";
+ }
+
+ var treeChildren = document.getElementById(treeChildrenId);
+ var name;
+ var i;
+
+ for (i = 0; i < treeChildren.children.length; i++) {
+ var item = treeChildren.children[i];
+ name = GetTreeItemAttributeStr(item);
+ if (name.toLowerCase() == attName.toLowerCase()) {
+ // Select item in the tree
+ // but don't trigger the tree's onSelect handler
+ gDoOnSelectTree = false;
+ try {
+ selectTreeItem(treeChildren, item);
+ } catch (e) {}
+ gDoOnSelectTree = true;
+
+ // Get the text in the "value' column treecell
+ return GetTreeItemValueStr(item);
+ }
+ }
+
+ // Attribute doesn't exist in tree, so remove selection
+ gDoOnSelectTree = false;
+ try {
+ treeChildren.parentNode.view.selection.clearSelection();
+ } catch (e) {}
+ gDoOnSelectTree = true;
+
+ return "";
+}
+
+/* Tree structure:
+ <treeItem>
+ <treeRow>
+ <treeCell> // Name Cell
+ <treeCell // Value Cell
+*/
+function GetTreeItemAttributeStr(treeItem) {
+ if (treeItem) {
+ return TrimString(
+ treeItem.firstElementChild.firstElementChild.getAttribute("label")
+ );
+ }
+
+ return "";
+}
+
+function GetTreeItemValueStr(treeItem) {
+ if (treeItem) {
+ return TrimString(
+ treeItem.firstElementChild.lastElementChild.getAttribute("label")
+ );
+ }
+
+ return "";
+}
+
+function SetTreeItemValueStr(treeItem, value) {
+ if (treeItem && GetTreeItemValueStr(treeItem) != value) {
+ treeItem.firstElementChild.lastElementChild.setAttribute("label", value);
+ }
+}
+
+function IsNotTreeHeader(treeCell) {
+ if (treeCell) {
+ return treeCell.parentNode.parentNode.nodeName != "treehead";
+ }
+
+ return false;
+}
+
+function RemoveNameFromAttArray(attName, attArray) {
+ for (var i = 0; i < attArray.length; i++) {
+ if (attName.toLowerCase() == attArray[i].toLowerCase()) {
+ // Remove 1 array item
+ attArray.splice(i, 1);
+ break;
+ }
+ }
+}
+
+// adds a generalised treeitem.
+function AddTreeItem(name, value, treeChildrenId, attArray) {
+ attArray[attArray.length] = name;
+ var treeChildren = document.getElementById(treeChildrenId);
+ var treeitem = document.createXULElement("treeitem");
+ var treerow = document.createXULElement("treerow");
+
+ var attrCell = document.createXULElement("treecell");
+ attrCell.setAttribute("class", "propertylist");
+ attrCell.setAttribute("label", name);
+
+ var valueCell = document.createXULElement("treecell");
+ valueCell.setAttribute("class", "propertylist");
+ valueCell.setAttribute("label", value);
+
+ treerow.appendChild(attrCell);
+ treerow.appendChild(valueCell);
+ treeitem.appendChild(treerow);
+ treeChildren.appendChild(treeitem);
+
+ // Select item just added, but suppress calling the onSelect handler.
+ gDoOnSelectTree = false;
+ try {
+ selectTreeItem(treeChildren, treeitem);
+ } catch (e) {}
+ gDoOnSelectTree = true;
+
+ return treeitem;
+}
+
+function selectTreeItem(treeChildren, item) {
+ var index = treeChildren.parentNode.view.getIndexOfItem(item);
+ treeChildren.parentNode.view.selection.select(index);
+}
+
+function getSelectedItem(tree) {
+ if (tree.view.selection.count == 1) {
+ return tree.view.getItemAtIndex(tree.currentIndex);
+ }
+ return null;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml
new file mode 100644
index 0000000000..cfeff95b42
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml
@@ -0,0 +1,243 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!-- first checkin of the year 2000! -->
+<!-- Ben Goodger, 12:50AM, 01/00/00 NZST -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/menulist.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EdAdvancedEdit.dtd">
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="min-width: 40em"
+ title="&WindowTitle.label;"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog id="advancedEditDlg">
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <!-- element page functions -->
+ <script src="chrome://messenger/content/messengercompose/EdAEHTMLAttributes.js" />
+ <script src="chrome://messenger/content/messengercompose/EdAECSSAttributes.js" />
+ <script src="chrome://messenger/content/messengercompose/EdAEJSEAttributes.js" />
+ <script src="chrome://messenger/content/messengercompose/EdAEAttributes.js" />
+
+ <!-- global dialog functions -->
+ <script src="chrome://messenger/content/messengercompose/EdAdvancedEdit.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <hbox>
+ <label value="&currentattributesfor.label;" />
+ <label class="header" id="tagLabel" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <tabbox flex="1">
+ <tabs>
+ <tab label="&tabHTML.label;" />
+ <tab label="&tabCSS.label;" />
+ <tab label="&tabJSE.label;" id="tabJSE" />
+ </tabs>
+ <tabpanels flex="1">
+ <!-- ============================================================== -->
+ <!-- HTML Attributes -->
+ <!-- ============================================================== -->
+ <vbox>
+ <tree
+ id="HTMLATree"
+ class="AttributesTree"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="single"
+ onselect="onSelectHTMLTreeItem();"
+ onclick="onSelectHTMLTreeItem();"
+ ondblclick="editHTMLAttributeValue(event.target);"
+ >
+ <treecols>
+ <treecol id="HTMLAttrCol" label="&tree.attributeHeader.label;" />
+ <splitter class="tree-splitter" />
+ <treecol id="HTMLValCol" label="&tree.valueHeader.label;" />
+ </treecols>
+ <treechildren id="HTMLAList" flex="1" />
+ </tree>
+ <hbox align="center">
+ <label value="&editAttribute.label;" />
+ <spacer flex="1" />
+ <button
+ label="&removeAttribute.label;"
+ oncommand="RemoveHTMLAttribute();"
+ />
+ </hbox>
+ <hbox>
+ <vbox flex="1">
+ <label
+ control="AddHTMLAttributeNameInput"
+ value="&AttName.label;"
+ />
+ <menulist
+ is="menulist-editable"
+ id="AddHTMLAttributeNameInput"
+ class="editorAdvancedEditableMenulist"
+ editable="true"
+ flex="1"
+ oninput="onInputHTMLAttributeName();"
+ oncommand="onInputHTMLAttributeName();"
+ />
+ </vbox>
+ <vbox flex="1">
+ <label
+ id="AddHTMLAttributeValueLabel"
+ control="AddHTMLAttributeValueInput"
+ value="&AttValue.label;"
+ />
+ <vbox flex="1">
+ <hbox flex="1" class="input-container">
+ <html:input
+ id="AddHTMLAttributeValueTextbox"
+ type="text"
+ class="input-inline"
+ onchange="onInputHTMLAttributeValue();"
+ aria-labelledby="AddHTMLAttributeValueLabel"
+ />
+ </hbox>
+ <hbox flex="1" collapsed="true">
+ <menulist
+ is="menulist-editable"
+ id="AddHTMLAttributeValueMenulist"
+ editable="true"
+ flex="1"
+ oninput="onInputHTMLAttributeValue();"
+ oncommand="onInputHTMLAttributeValue();"
+ />
+ </hbox>
+ </vbox>
+ </vbox>
+ </hbox>
+ </vbox>
+ <!-- ============================================================== -->
+ <!-- CSS Attributes -->
+ <!-- ============================================================== -->
+ <vbox>
+ <tree
+ id="CSSATree"
+ class="AttributesTree"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="single"
+ onselect="onSelectCSSTreeItem();"
+ onclick="onSelectCSSTreeItem();"
+ ondblclick="editCSSAttributeValue(event.target);"
+ >
+ <treecols>
+ <treecol id="CSSPropCol" label="&tree.propertyHeader.label;" />
+ <splitter class="tree-splitter" />
+ <treecol id="CSSValCol" label="&tree.valueHeader.label;" />
+ </treecols>
+ <treechildren id="CSSAList" flex="1" />
+ </tree>
+ <hbox align="center">
+ <label value="&editAttribute.label;" />
+ <spacer flex="1" />
+ <button
+ label="&removeAttribute.label;"
+ oncommand="RemoveCSSAttribute();"
+ />
+ </hbox>
+ <hbox>
+ <vbox flex="1">
+ <label
+ id="AddCSSAttributeNameLabel"
+ value="&PropertyName.label;"
+ />
+ <html:input
+ id="AddCSSAttributeNameInput"
+ type="text"
+ class="input-inline"
+ onchange="onInputCSSAttributeName();"
+ aria-labelledby="AddCSSAttributeNameLabel"
+ />
+ </vbox>
+ <vbox flex="1">
+ <label id="AddCSSAttributeValueLabel" value="&AttValue.label;" />
+ <html:input
+ id="AddCSSAttributeValueInput"
+ type="text"
+ class="input-inline"
+ onchange="onChangeCSSAttribute();"
+ aria-labelledby="AddCSSAttributeValueLabel"
+ />
+ </vbox>
+ </hbox>
+ </vbox>
+ <!-- ============================================================== -->
+ <!-- JavaScript Event Handlers -->
+ <!-- ============================================================== -->
+ <vbox>
+ <tree
+ id="JSEATree"
+ class="AttributesTree"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="single"
+ onselect="onSelectJSETreeItem();"
+ onclick="onSelectJSETreeItem();"
+ ondblclick="editJSEAttributeValue(event.target);"
+ >
+ <treecols>
+ <treecol id="AttrCol" label="&tree.attributeHeader.label;" />
+ <splitter class="tree-splitter" />
+ <treecol id="HeaderCol" label="&tree.valueHeader.label;" />
+ </treecols>
+ <treechildren id="JSEAList" flex="1" />
+ </tree>
+ <hbox align="center">
+ <label value="&editAttribute.label;" />
+ <spacer flex="1" />
+ <button
+ label="&removeAttribute.label;"
+ oncommand="RemoveJSEAttribute()"
+ />
+ </hbox>
+ <hbox>
+ <vbox flex="1">
+ <label value="&AttName.label;" />
+ <menulist
+ id="AddJSEAttributeNameList"
+ oncommand="onSelectJSEAttribute();"
+ />
+ </vbox>
+ <vbox flex="1">
+ <label id="AddJSEAttributeValueLabel" value="&AttValue.label;" />
+ <hbox flex="1" class="input-container">
+ <html:input
+ id="AddJSEAttributeValueInput"
+ type="text"
+ class="input-inline"
+ onchange="onInputJSEAttributeValue();"
+ aria-labelledby="AddJSEAttributeValueLabel"
+ />
+ </hbox>
+ </vbox>
+ </hbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdColorPicker.js b/comm/mail/components/compose/content/dialogs/EdColorPicker.js
new file mode 100644
index 0000000000..ef03a1d10b
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdColorPicker.js
@@ -0,0 +1,290 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+// Cancel() is in EdDialogCommon.js
+
+var insertNew = true;
+var tagname = "TAG NAME";
+var gColor = "";
+var LastPickedColor = "";
+var ColorType = "Text";
+var TextType = false;
+var HighlightType = false;
+var TableOrCell = false;
+var LastPickedIsDefault = true;
+var NoDefault = false;
+var gColorObj;
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancelColor);
+
+function Startup() {
+ if (!window.arguments[1]) {
+ dump("EdColorPicker: Missing color object param\n");
+ return;
+ }
+
+ // window.arguments[1] is object to get initial values and return color data
+ gColorObj = window.arguments[1];
+ gColorObj.Cancel = false;
+
+ gDialog.ColorPicker = document.getElementById("ColorPicker");
+ gDialog.ColorInput = document.getElementById("ColorInput");
+ gDialog.LastPickedButton = document.getElementById("LastPickedButton");
+ gDialog.LastPickedColor = document.getElementById("LastPickedColor");
+ gDialog.CellOrTableGroup = document.getElementById("CellOrTableGroup");
+ gDialog.TableRadio = document.getElementById("TableRadio");
+ gDialog.CellRadio = document.getElementById("CellRadio");
+ gDialog.ColorSwatch = document.getElementById("ColorPickerSwatch");
+ gDialog.Ok = document.querySelector("dialog").getButton("accept");
+
+ // The type of color we are setting:
+ // text: Text, Link, ActiveLink, VisitedLink,
+ // or background: Page, Table, or Cell
+ if (gColorObj.Type) {
+ ColorType = gColorObj.Type;
+ // Get string for dialog title from passed-in type
+ // (note constraint on editor.properties string name)
+ let IsCSSPrefChecked = Services.prefs.getBoolPref("editor.use_css");
+
+ if (GetCurrentEditor()) {
+ if (ColorType == "Page" && IsCSSPrefChecked && IsHTMLEditor()) {
+ document.title = GetString("BlockColor");
+ } else {
+ document.title = GetString(ColorType + "Color");
+ }
+ }
+ }
+
+ gDialog.ColorInput.value = "";
+ var tmpColor;
+ var haveTableRadio = false;
+
+ switch (ColorType) {
+ case "Page":
+ tmpColor = gColorObj.PageColor;
+ if (tmpColor && tmpColor.toLowerCase() != "window") {
+ gColor = tmpColor;
+ }
+ break;
+ case "Table":
+ if (gColorObj.TableColor) {
+ gColor = gColorObj.TableColor;
+ }
+ break;
+ case "Cell":
+ if (gColorObj.CellColor) {
+ gColor = gColorObj.CellColor;
+ }
+ break;
+ case "TableOrCell":
+ TableOrCell = true;
+ document.getElementById("TableOrCellGroup").collapsed = false;
+ haveTableRadio = true;
+ if (gColorObj.SelectedType == "Cell") {
+ gColor = gColorObj.CellColor;
+ gDialog.CellOrTableGroup.selectedItem = gDialog.CellRadio;
+ gDialog.CellRadio.focus();
+ } else {
+ gColor = gColorObj.TableColor;
+ gDialog.CellOrTableGroup.selectedItem = gDialog.TableRadio;
+ gDialog.TableRadio.focus();
+ }
+ break;
+ case "Highlight":
+ HighlightType = true;
+ if (gColorObj.HighlightColor) {
+ gColor = gColorObj.HighlightColor;
+ }
+ break;
+ default:
+ // Any other type will change some kind of text,
+ TextType = true;
+ tmpColor = gColorObj.TextColor;
+ if (tmpColor && tmpColor.toLowerCase() != "windowtext") {
+ gColor = gColorObj.TextColor;
+ }
+ break;
+ }
+
+ // Set initial color in input field and in the colorpicker
+ SetCurrentColor(gColor);
+ gDialog.ColorPicker.value = gColor;
+
+ // Use last-picked colors passed in, or those persistent on dialog
+ if (TextType) {
+ if (!("LastTextColor" in gColorObj) || !gColorObj.LastTextColor) {
+ gColorObj.LastTextColor =
+ gDialog.LastPickedColor.getAttribute("LastTextColor");
+ }
+ LastPickedColor = gColorObj.LastTextColor;
+ } else if (HighlightType) {
+ if (!("LastHighlightColor" in gColorObj) || !gColorObj.LastHighlightColor) {
+ gColorObj.LastHighlightColor =
+ gDialog.LastPickedColor.getAttribute("LastHighlightColor");
+ }
+ LastPickedColor = gColorObj.LastHighlightColor;
+ } else {
+ if (
+ !("LastBackgroundColor" in gColorObj) ||
+ !gColorObj.LastBackgroundColor
+ ) {
+ gColorObj.LastBackgroundColor = gDialog.LastPickedColor.getAttribute(
+ "LastBackgroundColor"
+ );
+ }
+ LastPickedColor = gColorObj.LastBackgroundColor;
+ }
+
+ // Set method to detect clicking on OK button
+ // so we don't get fooled by changing "default" behavior
+ gDialog.Ok.setAttribute("onclick", "SetDefaultToOk()");
+
+ if (!LastPickedColor) {
+ // Hide the button, as there is no last color available.
+ gDialog.LastPickedButton.hidden = true;
+ } else {
+ gDialog.LastPickedColor.setAttribute(
+ "style",
+ "background-color: " + LastPickedColor
+ );
+
+ // Make "Last-picked" the default button, until the user selects a color.
+ gDialog.Ok.removeAttribute("default");
+ gDialog.LastPickedButton.setAttribute("default", "true");
+ }
+
+ // Caller can prevent user from submitting an empty, i.e., default color
+ NoDefault = gColorObj.NoDefault;
+ if (NoDefault) {
+ // Hide the "Default button -- user must pick a color
+ document.getElementById("DefaultColorButton").collapsed = true;
+ }
+
+ // Set focus to colorpicker if not set to table radio buttons above
+ if (!haveTableRadio) {
+ gDialog.ColorPicker.focus();
+ }
+
+ SetWindowLocation();
+}
+
+function SelectColor() {
+ var color = gDialog.ColorPicker.value;
+ if (color) {
+ SetCurrentColor(color);
+ }
+}
+
+function RemoveColor() {
+ SetCurrentColor("");
+ gDialog.ColorInput.focus();
+ SetDefaultToOk();
+}
+
+function SelectColorByKeypress(aEvent) {
+ if (aEvent.charCode == aEvent.DOM_VK_SPACE) {
+ SelectColor();
+ SetDefaultToOk();
+ }
+}
+
+function SelectLastPickedColor() {
+ SetCurrentColor(LastPickedColor);
+ if (onAccept()) {
+ // window.close();
+ return true;
+ }
+
+ return false;
+}
+
+function SetCurrentColor(color) {
+ // TODO: Validate color?
+ if (!color) {
+ color = "";
+ }
+ gColor = TrimString(color).toLowerCase();
+ if (gColor == "mixed") {
+ gColor = "";
+ }
+ gDialog.ColorInput.value = gColor;
+ SetColorSwatch();
+}
+
+function SetColorSwatch() {
+ gDialog.ColorSwatch.setAttribute(
+ "style",
+ `background-color: ${TrimString(gDialog.ColorInput.value) || "inherit"}`
+ );
+}
+
+function SetDefaultToOk() {
+ gDialog.LastPickedButton.removeAttribute("default");
+ gDialog.Ok.setAttribute("default", "true");
+ LastPickedIsDefault = false;
+}
+
+function ValidateData() {
+ if (LastPickedIsDefault) {
+ gColor = LastPickedColor;
+ } else {
+ gColor = gDialog.ColorInput.value;
+ }
+
+ gColor = TrimString(gColor).toLowerCase();
+
+ // TODO: Validate the color string!
+
+ if (NoDefault && !gColor) {
+ ShowInputErrorMessage(GetString("NoColorError"));
+ SetTextboxFocus(gDialog.ColorInput);
+ return false;
+ }
+ return true;
+}
+
+function onAccept(event) {
+ if (!ValidateData()) {
+ event.preventDefault();
+ return;
+ }
+
+ // Set return values and save in persistent color attributes
+ if (TextType) {
+ gColorObj.TextColor = gColor;
+ if (gColor.length > 0) {
+ gDialog.LastPickedColor.setAttribute("LastTextColor", gColor);
+ gColorObj.LastTextColor = gColor;
+ }
+ } else if (HighlightType) {
+ gColorObj.HighlightColor = gColor;
+ if (gColor.length > 0) {
+ gDialog.LastPickedColor.setAttribute("LastHighlightColor", gColor);
+ gColorObj.LastHighlightColor = gColor;
+ }
+ } else {
+ gColorObj.BackgroundColor = gColor;
+ if (gColor.length > 0) {
+ gDialog.LastPickedColor.setAttribute("LastBackgroundColor", gColor);
+ gColorObj.LastBackgroundColor = gColor;
+ }
+ // If table or cell requested, tell caller which element to set on
+ if (TableOrCell && gDialog.TableRadio.selected) {
+ gColorObj.Type = "Table";
+ }
+ }
+ SaveWindowLocation();
+}
+
+function onCancelColor() {
+ // Tells caller that user canceled
+ gColorObj.Cancel = true;
+ SaveWindowLocation();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml b/comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml
new file mode 100644
index 0000000000..8576fc27da
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EdColorPicker.dtd">
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog>
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdColorPicker.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <hbox id="TableOrCellGroup" align="center" collapsed="true">
+ <label
+ control="CellOrTableGroup"
+ value="&background.label;"
+ accesskey="&background.accessKey;"
+ />
+ <radiogroup id="CellOrTableGroup" orient="horizontal">
+ <radio
+ id="TableRadio"
+ label="&table.label;"
+ accesskey="&table.accessKey;"
+ />
+ <radio
+ id="CellRadio"
+ label="&cell.label;"
+ accesskey="&cell.accessKey;"
+ />
+ </radiogroup>
+ </hbox>
+ <hbox align="center">
+ <label value="&chooseColor1.label;" />
+ <html:input
+ type="color"
+ id="ColorPicker"
+ onclick="SetDefaultToOk();"
+ ondblclick="if (onAccept()) { window.close(); }"
+ onkeypress="SelectColorByKeypress(event);"
+ onchange="SelectColor();"
+ />
+ <spacer flex="1" />
+ <button
+ id="LastPickedButton"
+ label="&lastPickedColor.label;"
+ accesskey="&lastPickedColor.accessKey;"
+ crop="right"
+ oncommand="SelectLastPickedColor();"
+ >
+ <spacer
+ id="LastPickedColor"
+ LastTextColor=""
+ LastBackgroundColor=""
+ persist="LastTextColor LastBackgroundColor"
+ />
+ </button>
+ </hbox>
+
+ <spacer class="spacer" />
+ <hbox align="center" flex="1">
+ <vbox>
+ <label
+ class="tip-caption"
+ value="&chooseColor2.label;"
+ accesskey="&chooseColor2.accessKey;"
+ control="ColorInput"
+ />
+ <label class="tip-caption" value="&setColorExample.label;" />
+ </vbox>
+ <html:input
+ id="ColorInput"
+ type="text"
+ style="width: 8em"
+ oninput="SetColorSwatch(); SetDefaultToOk();"
+ />
+ <label id="ColorPickerSwatch" />
+ <spacer flex="1" />
+ <button
+ id="DefaultColorButton"
+ label="&default.label;"
+ accesskey="&default.accessKey;"
+ oncommand="RemoveColor()"
+ />
+ </hbox>
+ <separator class="groove" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdColorProps.js b/comm/mail/components/compose/content/dialogs/EdColorProps.js
new file mode 100644
index 0000000000..c2635912d5
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdColorProps.js
@@ -0,0 +1,476 @@
+/* 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/. */
+
+/*
+ Behavior notes:
+ Radio buttons select "UseDefaultColors" vs. "UseCustomColors" modes.
+ If any color attribute is set in the body, mode is "Custom Colors",
+ even if 1 or more (but not all) are actually null (= "use default")
+ When in "Custom Colors" mode, all colors will be set on body tag,
+ even if they are just default colors, to assure compatible colors in page.
+ User cannot select "use default" for individual colors
+*/
+
+/* import-globals-from ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+// Cancel() is in EdDialogCommon.js
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+var gBodyElement;
+var prefs;
+var gBackgroundImage;
+
+// Initialize in case we can't get them from prefs???
+var defaultTextColor = "#000000";
+var defaultLinkColor = "#000099";
+var defaultActiveColor = "#000099";
+var defaultVisitedColor = "#990099";
+var defaultBackgroundColor = "#FFFFFF";
+const styleStr = "style";
+const textStr = "text";
+const linkStr = "link";
+const vlinkStr = "vlink";
+const alinkStr = "alink";
+const bgcolorStr = "bgcolor";
+const backgroundStr = "background";
+const cssColorStr = "color";
+const cssBackgroundColorStr = "background-color";
+const cssBackgroundImageStr = "background-image";
+const colorStyle = cssColorStr + ": ";
+const backColorStyle = cssBackgroundColorStr + ": ";
+const backImageStyle = "; " + cssBackgroundImageStr + ": url(";
+
+var customTextColor;
+var customLinkColor;
+var customActiveColor;
+var customVisitedColor;
+var customBackgroundColor;
+var previewBGColor;
+
+// dialog initialization code
+function Startup() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+
+ gDialog.ColorPreview = document.getElementById("ColorPreview");
+ gDialog.NormalText = document.getElementById("NormalText");
+ gDialog.LinkText = document.getElementById("LinkText");
+ gDialog.ActiveLinkText = document.getElementById("ActiveLinkText");
+ gDialog.VisitedLinkText = document.getElementById("VisitedLinkText");
+ gDialog.PageColorGroup = document.getElementById("PageColorGroup");
+ gDialog.DefaultColorsRadio = document.getElementById("DefaultColorsRadio");
+ gDialog.CustomColorsRadio = document.getElementById("CustomColorsRadio");
+ gDialog.BackgroundImageInput = document.getElementById(
+ "BackgroundImageInput"
+ );
+
+ try {
+ gBodyElement = editor.rootElement;
+ } catch (e) {}
+
+ if (!gBodyElement) {
+ dump("Failed to get BODY element!\n");
+ window.close();
+ }
+
+ // Set element we will edit
+ globalElement = gBodyElement.cloneNode(false);
+
+ // Initialize default colors from browser prefs
+ var browserColors = GetDefaultBrowserColors();
+ if (browserColors) {
+ // Use author's browser pref colors passed into dialog
+ defaultTextColor = browserColors.TextColor;
+ defaultLinkColor = browserColors.LinkColor;
+ defaultActiveColor = browserColors.ActiveLinkColor;
+ defaultVisitedColor = browserColors.VisitedLinkColor;
+ defaultBackgroundColor = browserColors.BackgroundColor;
+ }
+
+ // We only need to test for this once per dialog load
+ gHaveDocumentUrl = GetDocumentBaseUrl();
+
+ InitDialog();
+
+ gDialog.PageColorGroup.focus();
+
+ SetWindowLocation();
+}
+
+function InitDialog() {
+ // Get image from document
+ gBackgroundImage = GetHTMLOrCSSStyleValue(
+ globalElement,
+ backgroundStr,
+ cssBackgroundImageStr
+ );
+ if (/url\((.*)\)/.test(gBackgroundImage)) {
+ gBackgroundImage = RegExp.$1;
+ }
+
+ if (gBackgroundImage) {
+ // Shorten data URIs for display.
+ shortenImageData(gBackgroundImage, gDialog.BackgroundImageInput);
+ gDialog.ColorPreview.setAttribute(
+ styleStr,
+ backImageStyle + gBackgroundImage + ");"
+ );
+ }
+
+ SetRelativeCheckbox();
+
+ customTextColor = GetHTMLOrCSSStyleValue(globalElement, textStr, cssColorStr);
+ customTextColor = ConvertRGBColorIntoHEXColor(customTextColor);
+ customLinkColor = globalElement.getAttribute(linkStr);
+ customActiveColor = globalElement.getAttribute(alinkStr);
+ customVisitedColor = globalElement.getAttribute(vlinkStr);
+ customBackgroundColor = GetHTMLOrCSSStyleValue(
+ globalElement,
+ bgcolorStr,
+ cssBackgroundColorStr
+ );
+ customBackgroundColor = ConvertRGBColorIntoHEXColor(customBackgroundColor);
+
+ var haveCustomColor =
+ customTextColor ||
+ customLinkColor ||
+ customVisitedColor ||
+ customActiveColor ||
+ customBackgroundColor;
+
+ // Set default color explicitly for any that are missing
+ // PROBLEM: We are using "windowtext" and "window" for the Windows OS
+ // default color values. This works with CSS in preview window,
+ // but we should NOT use these as values for HTML attributes!
+
+ if (!customTextColor) {
+ customTextColor = defaultTextColor;
+ }
+ if (!customLinkColor) {
+ customLinkColor = defaultLinkColor;
+ }
+ if (!customActiveColor) {
+ customActiveColor = defaultActiveColor;
+ }
+ if (!customVisitedColor) {
+ customVisitedColor = defaultVisitedColor;
+ }
+ if (!customBackgroundColor) {
+ customBackgroundColor = defaultBackgroundColor;
+ }
+
+ if (haveCustomColor) {
+ // If any colors are set, then check the "Custom" radio button
+ gDialog.PageColorGroup.selectedItem = gDialog.CustomColorsRadio;
+ UseCustomColors();
+ } else {
+ gDialog.PageColorGroup.selectedItem = gDialog.DefaultColorsRadio;
+ UseDefaultColors();
+ }
+}
+
+function GetColorAndUpdate(ColorWellID) {
+ // Only allow selecting when in custom mode
+ if (!gDialog.CustomColorsRadio.selected) {
+ return;
+ }
+
+ var colorWell = document.getElementById(ColorWellID);
+ if (!colorWell) {
+ return;
+ }
+
+ // Don't allow a blank color, i.e., using the "default"
+ var colorObj = {
+ NoDefault: true,
+ Type: "",
+ TextColor: 0,
+ PageColor: 0,
+ Cancel: false,
+ };
+
+ switch (ColorWellID) {
+ case "textCW":
+ colorObj.Type = "Text";
+ colorObj.TextColor = customTextColor;
+ break;
+ case "linkCW":
+ colorObj.Type = "Link";
+ colorObj.TextColor = customLinkColor;
+ break;
+ case "activeCW":
+ colorObj.Type = "ActiveLink";
+ colorObj.TextColor = customActiveColor;
+ break;
+ case "visitedCW":
+ colorObj.Type = "VisitedLink";
+ colorObj.TextColor = customVisitedColor;
+ break;
+ case "backgroundCW":
+ colorObj.Type = "Page";
+ colorObj.PageColor = customBackgroundColor;
+ break;
+ }
+
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdColorPicker.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ "",
+ colorObj
+ );
+
+ // User canceled the dialog
+ if (colorObj.Cancel) {
+ return;
+ }
+
+ var color = "";
+ switch (ColorWellID) {
+ case "textCW":
+ color = customTextColor = colorObj.TextColor;
+ break;
+ case "linkCW":
+ color = customLinkColor = colorObj.TextColor;
+ break;
+ case "activeCW":
+ color = customActiveColor = colorObj.TextColor;
+ break;
+ case "visitedCW":
+ color = customVisitedColor = colorObj.TextColor;
+ break;
+ case "backgroundCW":
+ color = customBackgroundColor = colorObj.BackgroundColor;
+ break;
+ }
+
+ setColorWell(ColorWellID, color);
+ SetColorPreview(ColorWellID, color);
+}
+
+function SetColorPreview(ColorWellID, color) {
+ switch (ColorWellID) {
+ case "textCW":
+ gDialog.NormalText.setAttribute(styleStr, colorStyle + color);
+ break;
+ case "linkCW":
+ gDialog.LinkText.setAttribute(styleStr, colorStyle + color);
+ break;
+ case "activeCW":
+ gDialog.ActiveLinkText.setAttribute(styleStr, colorStyle + color);
+ break;
+ case "visitedCW":
+ gDialog.VisitedLinkText.setAttribute(styleStr, colorStyle + color);
+ break;
+ case "backgroundCW":
+ // Must combine background color and image style values
+ var styleValue = backColorStyle + color;
+ if (gBackgroundImage) {
+ styleValue += ";" + backImageStyle + gBackgroundImage + ");";
+ }
+
+ gDialog.ColorPreview.setAttribute(styleStr, styleValue);
+ previewBGColor = color;
+ break;
+ }
+}
+
+function UseCustomColors() {
+ SetElementEnabledById("TextButton", true);
+ SetElementEnabledById("LinkButton", true);
+ SetElementEnabledById("ActiveLinkButton", true);
+ SetElementEnabledById("VisitedLinkButton", true);
+ SetElementEnabledById("BackgroundButton", true);
+ SetElementEnabledById("Text", true);
+ SetElementEnabledById("Link", true);
+ SetElementEnabledById("Active", true);
+ SetElementEnabledById("Visited", true);
+ SetElementEnabledById("Background", true);
+
+ SetColorPreview("textCW", customTextColor);
+ SetColorPreview("linkCW", customLinkColor);
+ SetColorPreview("activeCW", customActiveColor);
+ SetColorPreview("visitedCW", customVisitedColor);
+ SetColorPreview("backgroundCW", customBackgroundColor);
+
+ setColorWell("textCW", customTextColor);
+ setColorWell("linkCW", customLinkColor);
+ setColorWell("activeCW", customActiveColor);
+ setColorWell("visitedCW", customVisitedColor);
+ setColorWell("backgroundCW", customBackgroundColor);
+}
+
+function UseDefaultColors() {
+ SetColorPreview("textCW", defaultTextColor);
+ SetColorPreview("linkCW", defaultLinkColor);
+ SetColorPreview("activeCW", defaultActiveColor);
+ SetColorPreview("visitedCW", defaultVisitedColor);
+ SetColorPreview("backgroundCW", defaultBackgroundColor);
+
+ // Setting to blank color will remove color from buttons,
+ setColorWell("textCW", "");
+ setColorWell("linkCW", "");
+ setColorWell("activeCW", "");
+ setColorWell("visitedCW", "");
+ setColorWell("backgroundCW", "");
+
+ // Disable color buttons and labels
+ SetElementEnabledById("TextButton", false);
+ SetElementEnabledById("LinkButton", false);
+ SetElementEnabledById("ActiveLinkButton", false);
+ SetElementEnabledById("VisitedLinkButton", false);
+ SetElementEnabledById("BackgroundButton", false);
+ SetElementEnabledById("Text", false);
+ SetElementEnabledById("Link", false);
+ SetElementEnabledById("Active", false);
+ SetElementEnabledById("Visited", false);
+ SetElementEnabledById("Background", false);
+}
+
+function chooseFile() {
+ // Get a local image file, converted into URL format
+ GetLocalFileURL("img").then(fileURL => {
+ // Always try to relativize local file URLs
+ if (gHaveDocumentUrl) {
+ fileURL = MakeRelativeUrl(fileURL);
+ }
+
+ gDialog.BackgroundImageInput.value = fileURL;
+
+ SetRelativeCheckbox();
+ ValidateAndPreviewImage(true);
+ SetTextboxFocus(gDialog.BackgroundImageInput);
+ });
+}
+
+function ChangeBackgroundImage() {
+ // Don't show error message for image while user is typing
+ ValidateAndPreviewImage(false);
+ SetRelativeCheckbox();
+}
+
+function ValidateAndPreviewImage(ShowErrorMessage) {
+ // First make a string with just background color
+ var styleValue = backColorStyle + previewBGColor + ";";
+
+ var retVal = true;
+ var image = TrimString(gDialog.BackgroundImageInput.value);
+ if (image) {
+ if (isImageDataShortened(image)) {
+ gBackgroundImage = restoredImageData(gDialog.BackgroundImageInput);
+ } else {
+ gBackgroundImage = image;
+
+ // Display must use absolute URL if possible
+ var displayImage = gHaveDocumentUrl ? MakeAbsoluteUrl(image) : image;
+ styleValue += backImageStyle + displayImage + ");";
+ }
+ } else {
+ gBackgroundImage = null;
+ }
+
+ // Set style on preview (removes image if not valid)
+ gDialog.ColorPreview.setAttribute(styleStr, styleValue);
+
+ // Note that an "empty" string is valid
+ return retVal;
+}
+
+function ValidateData() {
+ var editor = GetCurrentEditor();
+ try {
+ // Colors values are updated as they are picked, no validation necessary
+ if (gDialog.DefaultColorsRadio.selected) {
+ editor.removeAttributeOrEquivalent(globalElement, textStr, true);
+ globalElement.removeAttribute(linkStr);
+ globalElement.removeAttribute(vlinkStr);
+ globalElement.removeAttribute(alinkStr);
+ editor.removeAttributeOrEquivalent(globalElement, bgcolorStr, true);
+ } else {
+ // Do NOT accept the CSS "WindowsOS" color strings!
+ // Problem: We really should try to get the actual color values
+ // from windows, but I don't know how to do that!
+ var tmpColor = customTextColor.toLowerCase();
+ if (tmpColor != "windowtext") {
+ editor.setAttributeOrEquivalent(
+ globalElement,
+ textStr,
+ customTextColor,
+ true
+ );
+ } else {
+ editor.removeAttributeOrEquivalent(globalElement, textStr, true);
+ }
+
+ tmpColor = customBackgroundColor.toLowerCase();
+ if (tmpColor != "window") {
+ editor.setAttributeOrEquivalent(
+ globalElement,
+ bgcolorStr,
+ customBackgroundColor,
+ true
+ );
+ } else {
+ editor.removeAttributeOrEquivalent(globalElement, bgcolorStr, true);
+ }
+
+ globalElement.setAttribute(linkStr, customLinkColor);
+ globalElement.setAttribute(vlinkStr, customVisitedColor);
+ globalElement.setAttribute(alinkStr, customActiveColor);
+ }
+
+ if (ValidateAndPreviewImage(true)) {
+ // A valid image may be null for no image
+ if (gBackgroundImage) {
+ globalElement.setAttribute(backgroundStr, gBackgroundImage);
+ } else {
+ editor.removeAttributeOrEquivalent(globalElement, backgroundStr, true);
+ }
+
+ return true;
+ }
+ } catch (e) {}
+ return false;
+}
+
+function onAccept(event) {
+ // If it's a file, convert to a data URL.
+ if (gBackgroundImage && /^file:/i.test(gBackgroundImage)) {
+ let nsFile = Services.io
+ .newURI(gBackgroundImage)
+ .QueryInterface(Ci.nsIFileURL).file;
+ if (nsFile.exists()) {
+ let reader = new FileReader();
+ reader.addEventListener("load", function () {
+ gBackgroundImage = reader.result;
+ gDialog.BackgroundImageInput.value = reader.result;
+ if (onAccept(event)) {
+ window.close();
+ }
+ });
+ File.createFromNsIFile(nsFile).then(file => {
+ reader.readAsDataURL(file);
+ });
+ event.preventDefault(); // Don't close just yet...
+ return false;
+ }
+ }
+ if (ValidateData()) {
+ // Copy attributes to element we are changing
+ try {
+ GetCurrentEditor().cloneAttributes(gBodyElement, globalElement);
+ } catch (e) {}
+
+ SaveWindowLocation();
+ return true; // do close the window
+ }
+ event.preventDefault();
+ return false;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdColorProps.xhtml b/comm/mail/components/compose/content/dialogs/EdColorProps.xhtml
new file mode 100644
index 0000000000..633b1639d9
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdColorProps.xhtml
@@ -0,0 +1,211 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % edColorPropertiesDTD SYSTEM "chrome://messenger/locale/messengercompose/EditorColorProperties.dtd">
+%edColorPropertiesDTD;
+<!ENTITY % composeEditorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/mailComposeEditorOverlay.dtd">
+%composeEditorOverlayDTD;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdColorProps.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <html:fieldset align="start">
+ <html:legend>&pageColors.label;</html:legend>
+ <radiogroup id="PageColorGroup">
+ <radio
+ id="DefaultColorsRadio"
+ label="&defaultColorsRadio.label;"
+ oncommand="UseDefaultColors()"
+ accesskey="&defaultColorsRadio.accessKey;"
+ tooltiptext="&defaultColorsRadio.tooltip;"
+ />
+ <radio
+ id="CustomColorsRadio"
+ label="&customColorsRadio.label;"
+ oncommand="UseCustomColors()"
+ accesskey="&customColorsRadio.accessKey;"
+ tooltiptext="&customColorsRadio.tooltip;"
+ />
+ </radiogroup>
+ <hbox class="indent">
+ <hbox>
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ id="Text"
+ control="TextButton"
+ value="&normalText.label;&colon.character;"
+ accesskey="&normalText.accessKey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="Link"
+ flex="1"
+ control="LinkButton"
+ value="&linkText.label;&colon.character;"
+ accesskey="&linkText.accessKey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="Active"
+ flex="1"
+ control="ActiveLinkButton"
+ value="&activeLinkText.label;&colon.character;"
+ accesskey="&activeLinkText.accessKey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="Visited"
+ flex="1"
+ control="VisitedLinkButton"
+ value="&visitedLinkText.label;&colon.character;"
+ accesskey="&visitedLinkText.accessKey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="Background"
+ flex="1"
+ control="BackgroundButton"
+ value="&background.label;"
+ accesskey="&background.accessKey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <button
+ id="TextButton"
+ class="color-button"
+ oncommand="GetColorAndUpdate('textCW');"
+ >
+ <spacer id="textCW" class="color-well" />
+ </button>
+ <button
+ id="LinkButton"
+ class="color-button"
+ oncommand="GetColorAndUpdate('linkCW');"
+ >
+ <spacer id="linkCW" class="color-well" />
+ </button>
+ <button
+ id="ActiveLinkButton"
+ class="color-button"
+ oncommand="GetColorAndUpdate('activeCW');"
+ >
+ <spacer id="activeCW" class="color-well" />
+ </button>
+ <button
+ id="VisitedLinkButton"
+ class="color-button"
+ oncommand="GetColorAndUpdate('visitedCW');"
+ >
+ <spacer id="visitedCW" class="color-well" />
+ </button>
+ <button
+ id="BackgroundButton"
+ class="color-button"
+ oncommand="GetColorAndUpdate('backgroundCW');"
+ >
+ <spacer id="backgroundCW" class="color-well" />
+ </button>
+ </vbox>
+ </hbox>
+ <vbox id="ColorPreview">
+ <spacer flex="1" />
+ <label class="larger" id="NormalText" value="&normalText.label;" />
+ <spacer flex="1" />
+ <label class="larger" id="LinkText" value="&linkText.label;" />
+ <spacer flex="1" />
+ <label
+ class="larger"
+ id="ActiveLinkText"
+ value="&activeLinkText.label;"
+ />
+ <spacer flex="1" />
+ <label
+ class="larger"
+ id="VisitedLinkText"
+ value="&visitedLinkText.label;"
+ />
+ <spacer flex="1" />
+ </vbox>
+ <spacer flex="1" />
+ </hbox>
+ <spacer class="spacer" />
+ </html:fieldset>
+ <spacer class="spacer" />
+ <label
+ control="BackgroundImageInput"
+ value="&backgroundImage.label;"
+ tooltiptext="&backgroundImage.tooltip;"
+ accesskey="&backgroundImage.accessKey;"
+ />
+ <tooltip id="shortenedDataURI">
+ <label value="&backgroundImage.shortenedDataURI;" />
+ </tooltip>
+ <html:input
+ id="BackgroundImageInput"
+ type="text"
+ class="uri-element input-inline"
+ onchange="ChangeBackgroundImage()"
+ aria-label="&backgroundImage.tooltip;"
+ />
+ <hbox align="center">
+ <checkbox
+ id="MakeRelativeCheckbox"
+ for="BackgroundImageInput"
+ label="&makeUrlRelative.label;"
+ accesskey="&makeUrlRelative.accessKey;"
+ oncommand="MakeInputValueRelativeOrAbsolute(this);"
+ tooltiptext="&makeUrlRelative.tooltip;"
+ />
+ <spacer flex="1" />
+ <button
+ id="ChooseFile"
+ oncommand="chooseFile()"
+ label="&chooseFileButton.label;"
+ accesskey="&chooseFileButton.accessKey;"
+ />
+ </hbox>
+ <spacer class="smallspacer" />
+ <hbox>
+ <spacer flex="1" />
+ <button
+ id="AdvancedEditButton"
+ oncommand="onAdvancedEdit();"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <separator class="groove" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdConvertToTable.js b/comm/mail/components/compose/content/dialogs/EdConvertToTable.js
new file mode 100644
index 0000000000..e7f19cff67
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdConvertToTable.js
@@ -0,0 +1,325 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+var gIndex;
+var gCommaIndex = "0";
+var gSpaceIndex = "1";
+var gOtherIndex = "2";
+
+// dialog initialization code
+function Startup() {
+ if (!GetCurrentEditor()) {
+ window.close();
+ return;
+ }
+
+ gDialog.sepRadioGroup = document.getElementById("SepRadioGroup");
+ gDialog.sepCharacterInput = document.getElementById("SepCharacterInput");
+ gDialog.deleteSepCharacter = document.getElementById("DeleteSepCharacter");
+ gDialog.collapseSpaces = document.getElementById("CollapseSpaces");
+
+ // We persist the user's separator character
+ gDialog.sepCharacterInput.value =
+ gDialog.sepRadioGroup.getAttribute("character");
+
+ gIndex = gDialog.sepRadioGroup.getAttribute("index");
+
+ switch (gIndex) {
+ case gCommaIndex:
+ default:
+ gDialog.sepRadioGroup.selectedItem = document.getElementById("comma");
+ break;
+ case gSpaceIndex:
+ gDialog.sepRadioGroup.selectedItem = document.getElementById("space");
+ break;
+ case gOtherIndex:
+ gDialog.sepRadioGroup.selectedItem = document.getElementById("other");
+ break;
+ }
+
+ // Set initial enable state on character input and "collapse" checkbox
+ SelectCharacter(gIndex);
+
+ SetWindowLocation();
+}
+
+function InputSepCharacter() {
+ var str = gDialog.sepCharacterInput.value;
+
+ // Limit input to 1 character
+ if (str.length > 1) {
+ str = str.slice(0, 1);
+ }
+
+ // We can never allow tag or entity delimiters for separator character
+ if (str == "<" || str == ">" || str == "&" || str == ";" || str == " ") {
+ str = "";
+ }
+
+ gDialog.sepCharacterInput.value = str;
+}
+
+function SelectCharacter(radioGroupIndex) {
+ gIndex = radioGroupIndex;
+ SetElementEnabledById("SepCharacterInput", gIndex == gOtherIndex);
+ SetElementEnabledById("CollapseSpaces", gIndex == gSpaceIndex);
+}
+
+/* eslint-disable complexity */
+function onAccept() {
+ var sepCharacter = "";
+ switch (gIndex) {
+ case gCommaIndex:
+ sepCharacter = ",";
+ break;
+ case gSpaceIndex:
+ sepCharacter = " ";
+ break;
+ case gOtherIndex:
+ sepCharacter = gDialog.sepCharacterInput.value.slice(0, 1);
+ break;
+ }
+
+ var editor = GetCurrentEditor();
+ var str;
+ try {
+ str = editor.outputToString(
+ "text/html",
+ kOutputLFLineBreak | kOutputSelectionOnly
+ );
+ } catch (e) {}
+ if (!str) {
+ SaveWindowLocation();
+ return;
+ }
+
+ // Replace nbsp with spaces:
+ str = str.replace(/\u00a0/g, " ");
+
+ // Strip out </p> completely
+ str = str.replace(/\s*<\/p>\s*/g, "");
+
+ // Trim whitespace adjacent to <p> and <br> tags
+ // and replace <p> with <br>
+ // (which will be replaced with </tr> below)
+ str = str.replace(/\s*<p>\s*|\s*<br>\s*/g, "<br>");
+
+ // Trim leading <br>s
+ str = str.replace(/^(<br>)+/, "");
+
+ // Trim trailing <br>s
+ str = str.replace(/(<br>)+$/, "");
+
+ // Reduce multiple internal <br> to just 1
+ // TODO: Maybe add a checkbox to let user decide
+ // str = str.replace(/(<br>)+/g, "<br>");
+
+ // Trim leading and trailing spaces
+ str = str.trim();
+
+ // Remove all tag contents so we don't replace
+ // separator character within tags
+ // Also converts lists to something useful
+ var stack = [];
+ var start;
+ var end;
+ var searchStart = 0;
+ var listSeparator = "";
+ var listItemSeparator = "";
+ var endList = false;
+
+ do {
+ start = str.indexOf("<", searchStart);
+
+ if (start >= 0) {
+ end = str.indexOf(">", start + 1);
+ if (end > start) {
+ let tagContent = str.slice(start + 1, end).trim();
+
+ if (/^ol|^ul|^dl/.test(tagContent)) {
+ // Replace list tag with <BR> to start new row
+ // at beginning of second or greater list tag
+ str = str.slice(0, start) + listSeparator + str.slice(end + 1);
+ if (listSeparator == "") {
+ listSeparator = "<br>";
+ }
+
+ // Reset for list item separation into cells
+ listItemSeparator = "";
+ } else if (/^li|^dt|^dd/.test(tagContent)) {
+ // Start a new row if this is first item after the ending the last list
+ if (endList) {
+ listItemSeparator = "<br>";
+ }
+
+ // Start new cell at beginning of second or greater list items
+ str = str.slice(0, start) + listItemSeparator + str.slice(end + 1);
+
+ if (endList || listItemSeparator == "") {
+ listItemSeparator = sepCharacter;
+ }
+
+ endList = false;
+ } else {
+ // Find end tags
+ endList = /^\/ol|^\/ul|^\/dl/.test(tagContent);
+ if (endList || /^\/li|^\/dt|^\/dd/.test(tagContent)) {
+ // Strip out tag
+ str = str.slice(0, start) + str.slice(end + 1);
+ } else {
+ // Not a list-related tag: Store tag contents in an array
+ stack.push(tagContent);
+
+ // Keep the "<" and ">" while removing from source string
+ start++;
+ str = str.slice(0, start) + str.slice(end);
+ }
+ }
+ }
+ searchStart = start + 1;
+ }
+ } while (start >= 0);
+
+ // Replace separator characters with table cells
+ var replaceString;
+ if (gDialog.deleteSepCharacter.checked) {
+ replaceString = "";
+ } else {
+ // Don't delete separator character,
+ // so include it at start of string to replace
+ replaceString = sepCharacter;
+ }
+
+ replaceString += "<td>";
+
+ if (sepCharacter.length > 0) {
+ var tempStr = sepCharacter;
+ var regExpChars = ".!@#$%^&*-+[]{}()|\\/";
+ if (regExpChars.includes(sepCharacter)) {
+ tempStr = "\\" + sepCharacter;
+ }
+
+ if (gIndex == gSpaceIndex) {
+ // If checkbox is checked,
+ // one or more adjacent spaces are one separator
+ if (gDialog.collapseSpaces.checked) {
+ tempStr = "\\s+";
+ } else {
+ tempStr = "\\s";
+ }
+ }
+ var pattern = new RegExp(tempStr, "g");
+ str = str.replace(pattern, replaceString);
+ }
+
+ // Put back tag contents that we removed above
+ searchStart = 0;
+ var stackIndex = 0;
+ do {
+ start = str.indexOf("<", searchStart);
+ end = start + 1;
+ if (start >= 0 && str.charAt(end) == ">") {
+ // We really need a FIFO stack!
+ str = str.slice(0, end) + stack[stackIndex++] + str.slice(end);
+ }
+ searchStart = end;
+ } while (start >= 0);
+
+ // End table row and start another for each br or p
+ str = str.replace(/\s*<br>\s*/g, "</tr>\n<tr><td>");
+
+ // Add the table tags and the opening and closing tr/td tags
+ // Default table attributes should be same as those used in nsHTMLEditor::CreateElementWithDefaults()
+ // (Default width="100%" is used in EdInsertTable.js)
+ str =
+ '<table border="1" width="100%" cellpadding="2" cellspacing="2">\n<tr><td>' +
+ str +
+ "</tr>\n</table>\n";
+
+ editor.beginTransaction();
+
+ // Delete the selection -- makes it easier to find where table will insert
+ var nodeBeforeTable = null;
+ var nodeAfterTable = null;
+ try {
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+
+ var anchorNodeBeforeInsert = editor.selection.anchorNode;
+ var offset = editor.selection.anchorOffset;
+ if (anchorNodeBeforeInsert.nodeType == Node.TEXT_NODE) {
+ // Text was split. Table should be right after the first or before
+ nodeBeforeTable = anchorNodeBeforeInsert.previousSibling;
+ nodeAfterTable = anchorNodeBeforeInsert;
+ } else {
+ // Table should be inserted right after node pointed to by selection
+ if (offset > 0) {
+ nodeBeforeTable = anchorNodeBeforeInsert.childNodes.item(offset - 1);
+ }
+
+ nodeAfterTable = anchorNodeBeforeInsert.childNodes.item(offset);
+ }
+
+ editor.insertHTML(str);
+ } catch (e) {}
+
+ var table = null;
+ if (nodeAfterTable) {
+ var previous = nodeAfterTable.previousSibling;
+ if (previous && previous.nodeName.toLowerCase() == "table") {
+ table = previous;
+ }
+ }
+ if (!table && nodeBeforeTable) {
+ var next = nodeBeforeTable.nextSibling;
+ if (next && next.nodeName.toLowerCase() == "table") {
+ table = next;
+ }
+ }
+
+ if (table) {
+ // Fixup table only if pref is set
+ var firstRow;
+ try {
+ if (Services.prefs.getBoolPref("editor.table.maintain_structure")) {
+ editor.normalizeTable(table);
+ }
+
+ firstRow = editor.getFirstRow(table);
+ } catch (e) {}
+
+ // Put caret in first cell
+ if (firstRow) {
+ var node2 = firstRow.firstChild;
+ do {
+ if (
+ node2.nodeName.toLowerCase() == "td" ||
+ node2.nodeName.toLowerCase() == "th"
+ ) {
+ try {
+ editor.selection.collapse(node2, 0);
+ } catch (e) {}
+ break;
+ }
+ node2 = node2.nextSibling;
+ } while (node2);
+ }
+ }
+
+ editor.endTransaction();
+
+ // Save persisted attributes
+ gDialog.sepRadioGroup.setAttribute("index", gIndex);
+ if (gIndex == gOtherIndex) {
+ gDialog.sepRadioGroup.setAttribute("character", sepCharacter);
+ }
+
+ SaveWindowLocation();
+}
+/* eslint-enable complexity */
diff --git a/comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml b/comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml
new file mode 100644
index 0000000000..6f2d9ad5b1
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EdConvertToTable.dtd">
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="Startup()"
+ lightweightthemes="true"
+ style="min-width: 20em"
+>
+ <dialog>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <!--- Element-specific methods -->
+ <script src="chrome://messenger/content/messengercompose/EdConvertToTable.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+ <description class="wrap" flex="1">&instructions1.label;</description>
+ <description class="wrap" flex="1">&instructions2.label;</description>
+ <radiogroup
+ id="SepRadioGroup"
+ persist="index character"
+ index="0"
+ character=""
+ >
+ <radio
+ id="comma"
+ label="&commaRadio.label;"
+ oncommand="SelectCharacter('0');"
+ />
+ <radio
+ id="space"
+ label="&spaceRadio.label;"
+ oncommand="SelectCharacter('1');"
+ />
+ <hbox>
+ <spacer class="radio-spacer" />
+ <checkbox
+ id="CollapseSpaces"
+ label="&collapseSpaces.label;"
+ checked="true"
+ persist="checked"
+ tooltiptext="&collapseSpaces.tooltip;"
+ />
+ </hbox>
+ <hbox align="center">
+ <radio
+ id="other"
+ label="&otherRadio.label;"
+ oncommand="SelectCharacter('2');"
+ />
+ <html:input
+ id="SepCharacterInput"
+ type="text"
+ aria-labelledby="other"
+ class="narrow input-inline"
+ oninput="InputSepCharacter()"
+ />
+ </hbox>
+ </radiogroup>
+ <spacer class="spacer" />
+ <checkbox
+ id="DeleteSepCharacter"
+ label="&deleteCharCheck.label;"
+ persist="checked"
+ />
+ <spacer class="spacer" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdDialogCommon.js b/comm/mail/components/compose/content/dialogs/EdDialogCommon.js
new file mode 100644
index 0000000000..ce377e4bbf
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdDialogCommon.js
@@ -0,0 +1,679 @@
+/* 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/. */
+
+// Each editor window must include this file
+
+/* import-globals-from ../editorUtilities.js */
+/* globals InitDialog, ChangeLinkLocation, ValidateData */
+
+// Object to attach commonly-used widgets (all dialogs should use this)
+var gDialog = {};
+
+var gHaveDocumentUrl = false;
+var gValidationError = false;
+
+// Use for 'defaultIndex' param in InitPixelOrPercentMenulist
+const gPixel = 0;
+const gPercent = 1;
+
+const gMaxPixels = 100000; // Used for image size, borders, spacing, and padding
+// Gecko code uses 1000 for maximum rowspan, colspan
+// Also, editing performance is really bad above this
+const gMaxRows = 1000;
+const gMaxColumns = 1000;
+const gMaxTableSize = 1000000; // Width or height of table or cells
+
+// A XUL element with id="location" for managing
+// dialog location relative to parent window
+var gLocation;
+
+// The element being edited - so AdvancedEdit can have access to it
+var globalElement;
+
+/* Validate contents of an input field
+ *
+ * inputWidget The 'input' element for the the attribute's value
+ * listWidget The 'menulist' XUL element for choosing "pixel" or "percent"
+ * May be null when no pixel/percent is used.
+ * minVal minimum allowed for input widget's value
+ * maxVal maximum allowed for input widget's value
+ * (when "listWidget" is used, maxVal is used for "pixel" maximum,
+ * 100% is assumed if "percent" is the user's choice)
+ * element The DOM element that we set the attribute on. May be null.
+ * attName Name of the attribute to set. May be null or ignored if "element" is null
+ * mustHaveValue If true, error dialog is displayed if "value" is empty string
+ *
+ * This calls "ValidateNumberRange()", which puts up an error dialog to inform the user.
+ * If error, we also:
+ * Shift focus and select contents of the inputWidget,
+ * Switch to appropriate panel of tabbed dialog if user implements "SwitchToValidate()",
+ * and/or will expand the dialog to full size if "More / Fewer" feature is implemented
+ *
+ * Returns the "value" as a string, or "" if error or input contents are empty
+ * The global "gValidationError" variable is set true if error was found
+ */
+function ValidateNumber(
+ inputWidget,
+ listWidget,
+ minVal,
+ maxVal,
+ element,
+ attName,
+ mustHaveValue
+) {
+ if (!inputWidget) {
+ gValidationError = true;
+ return "";
+ }
+
+ // Global error return value
+ gValidationError = false;
+ var maxLimit = maxVal;
+ var isPercent = false;
+
+ var numString = TrimString(inputWidget.value);
+ if (numString || mustHaveValue) {
+ if (listWidget) {
+ isPercent = listWidget.selectedIndex == 1;
+ }
+ if (isPercent) {
+ maxLimit = 100;
+ }
+
+ // This method puts up the error message
+ numString = ValidateNumberRange(numString, minVal, maxLimit, mustHaveValue);
+ if (!numString) {
+ // Switch to appropriate panel for error reporting
+ SwitchToValidatePanel();
+
+ // Error - shift to offending input widget
+ SetTextboxFocus(inputWidget);
+ gValidationError = true;
+ } else {
+ if (isPercent) {
+ numString += "%";
+ }
+ if (element) {
+ GetCurrentEditor().setAttributeOrEquivalent(
+ element,
+ attName,
+ numString,
+ true
+ );
+ }
+ }
+ } else if (element) {
+ GetCurrentEditor().removeAttributeOrEquivalent(element, attName, true);
+ }
+ return numString;
+}
+
+/* Validate contents of an input field
+ *
+ * value number to validate
+ * minVal minimum allowed for input widget's value
+ * maxVal maximum allowed for input widget's value
+ * (when "listWidget" is used, maxVal is used for "pixel" maximum,
+ * 100% is assumed if "percent" is the user's choice)
+ * mustHaveValue If true, error dialog is displayed if "value" is empty string
+ *
+ * If inputWidget's value is outside of range, or is empty when "mustHaveValue" = true,
+ * an error dialog is popuped up to inform the user. The focus is shifted
+ * to the inputWidget.
+ *
+ * Returns the "value" as a string, or "" if error or input contents are empty
+ * The global "gValidationError" variable is set true if error was found
+ */
+function ValidateNumberRange(value, minValue, maxValue, mustHaveValue) {
+ // Initialize global error flag
+ gValidationError = false;
+ value = TrimString(String(value));
+
+ // We don't show error for empty string unless caller wants to
+ if (!value && !mustHaveValue) {
+ return "";
+ }
+
+ var numberStr = "";
+
+ if (value.length > 0) {
+ // Extract just numeric characters
+ var number = Number(value.replace(/\D+/g, ""));
+ if (number >= minValue && number <= maxValue) {
+ // Return string version of the number
+ return String(number);
+ }
+ numberStr = String(number);
+ }
+
+ var message = "";
+
+ if (numberStr.length > 0) {
+ // We have a number from user outside of allowed range
+ message = GetString("ValidateRangeMsg");
+ message = message.replace(/%n%/, numberStr);
+ message += "\n ";
+ }
+ message += GetString("ValidateNumberMsg");
+
+ // Replace variable placeholders in message with number values
+ message = message.replace(/%min%/, minValue).replace(/%max%/, maxValue);
+ ShowInputErrorMessage(message);
+
+ // Return an empty string to indicate error
+ gValidationError = true;
+ return "";
+}
+
+function SetTextboxFocusById(id) {
+ SetTextboxFocus(document.getElementById(id));
+}
+
+function SetTextboxFocus(input) {
+ if (input) {
+ input.focus();
+ }
+}
+
+function ShowInputErrorMessage(message) {
+ Services.prompt.alert(window, GetString("InputError"), message);
+ window.focus();
+}
+
+// Get the text appropriate to parent container
+// to determine what a "%" value is referring to.
+// elementForAtt is element we are actually setting attributes on
+// (a temporary copy of element in the doc to allow canceling),
+// but elementInDoc is needed to find parent context in document
+function GetAppropriatePercentString(elementForAtt, elementInDoc) {
+ var editor = GetCurrentEditor();
+ try {
+ var name = elementForAtt.nodeName.toLowerCase();
+ if (name == "td" || name == "th") {
+ return GetString("PercentOfTable");
+ }
+
+ // Check if element is within a table cell
+ if (editor.getElementOrParentByTagName("td", elementInDoc)) {
+ return GetString("PercentOfCell");
+ }
+ return GetString("PercentOfWindow");
+ } catch (e) {
+ return "";
+ }
+}
+
+function ClearListbox(listbox) {
+ if (listbox) {
+ listbox.clearSelection();
+ while (listbox.hasChildNodes()) {
+ listbox.lastChild.remove();
+ }
+ }
+}
+
+function forceInteger(elementID) {
+ var editField = document.getElementById(elementID);
+ if (!editField) {
+ return;
+ }
+
+ var stringIn = editField.value;
+ if (stringIn && stringIn.length > 0) {
+ // Strip out all nonnumeric characters
+ stringIn = stringIn.replace(/\D+/g, "");
+ if (!stringIn) {
+ stringIn = "";
+ }
+
+ // Write back only if changed
+ if (stringIn != editField.value) {
+ editField.value = stringIn;
+ }
+ }
+}
+
+function InitPixelOrPercentMenulist(
+ elementForAtt,
+ elementInDoc,
+ attribute,
+ menulistID,
+ defaultIndex
+) {
+ if (!defaultIndex) {
+ defaultIndex = gPixel;
+ }
+
+ // var size = elementForAtt.getAttribute(attribute);
+ var size = GetHTMLOrCSSStyleValue(elementForAtt, attribute, attribute);
+ var menulist = document.getElementById(menulistID);
+ var pixelItem;
+ var percentItem;
+
+ if (!menulist) {
+ dump("NO MENULIST found for ID=" + menulistID + "\n");
+ return size;
+ }
+
+ menulist.removeAllItems();
+ pixelItem = menulist.appendItem(GetString("Pixels"));
+
+ if (!pixelItem) {
+ return 0;
+ }
+
+ percentItem = menulist.appendItem(
+ GetAppropriatePercentString(elementForAtt, elementInDoc)
+ );
+ if (size && size.length > 0) {
+ // Search for a "%" or "px"
+ if (size.includes("%")) {
+ // Strip out the %
+ size = size.substr(0, size.indexOf("%"));
+ if (percentItem) {
+ menulist.selectedItem = percentItem;
+ }
+ } else {
+ if (size.includes("px")) {
+ // Strip out the px
+ size = size.substr(0, size.indexOf("px"));
+ }
+ menulist.selectedItem = pixelItem;
+ }
+ } else {
+ menulist.selectedIndex = defaultIndex;
+ }
+
+ return size;
+}
+
+function onAdvancedEdit() {
+ // First validate data from widgets in the "simpler" property dialog
+ if (ValidateData()) {
+ // Set true if OK is clicked in the Advanced Edit dialog
+ window.AdvancedEditOK = false;
+ // Open the AdvancedEdit dialog, passing in the element to be edited
+ // (the copy named "globalElement")
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdAdvancedEdit.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal,resizable=yes",
+ "",
+ globalElement
+ );
+ window.focus();
+ if (window.AdvancedEditOK) {
+ // Copy edited attributes to the dialog widgets:
+ InitDialog();
+ }
+ }
+}
+
+function getColor(ColorPickerID) {
+ var colorPicker = document.getElementById(ColorPickerID);
+ var color;
+ if (colorPicker) {
+ // Extract color from colorPicker and assign to colorWell.
+ color = colorPicker.getAttribute("color");
+ if (color && color == "") {
+ return null;
+ }
+ // Clear color so next if it's called again before
+ // color picker is actually used, we dedect the "don't set color" state
+ colorPicker.setAttribute("color", "");
+ }
+
+ return color;
+}
+
+function setColorWell(ColorWellID, color) {
+ var colorWell = document.getElementById(ColorWellID);
+ if (colorWell) {
+ if (!color || color == "") {
+ // Don't set color (use default)
+ // Trigger change to not show color swatch
+ colorWell.setAttribute("default", "true");
+ // Style in CSS sets "background-color",
+ // but color won't clear unless we do this:
+ colorWell.removeAttribute("style");
+ } else {
+ colorWell.removeAttribute("default");
+ // Use setAttribute so colorwell can be a XUL element, such as button
+ colorWell.setAttribute("style", "background-color:" + color);
+ }
+ }
+}
+
+function SwitchToValidatePanel() {
+ // no default implementation
+ // Only EdTableProps.js currently implements this
+}
+
+/**
+ * @returns {Promise} URL spec of the file chosen, or null
+ */
+function GetLocalFileURL(filterType) {
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ var fileType = "html";
+
+ if (filterType == "img") {
+ fp.init(window, GetString("SelectImageFile"), Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterImages);
+ fileType = "image";
+ } else if (filterType.startsWith("html")) {
+ // Current usage of this is in Link dialog,
+ // where we always want HTML first
+ fp.init(window, GetString("OpenHTMLFile"), Ci.nsIFilePicker.modeOpen);
+
+ // When loading into Composer, direct user to prefer HTML files and text files,
+ // so we call separately to control the order of the filter list
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ fp.appendFilters(Ci.nsIFilePicker.filterText);
+
+ // Link dialog also allows linking to images
+ if (filterType.includes("img", 1)) {
+ fp.appendFilters(Ci.nsIFilePicker.filterImages);
+ }
+ }
+ // Default or last filter is "All Files"
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ // set the file picker's current directory to last-opened location saved in prefs
+ SetFilePickerDirectory(fp, fileType);
+
+ return new Promise(resolve => {
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.file) {
+ resolve(null);
+ return;
+ }
+ SaveFilePickerDirectory(fp, fileType);
+ resolve(fp.fileURL.spec);
+ });
+ });
+}
+
+function SetWindowLocation() {
+ gLocation = document.getElementById("location");
+ if (gLocation) {
+ const screenX = Math.max(
+ 0,
+ Math.min(
+ window.opener.screenX + Number(gLocation.getAttribute("offsetX")),
+ screen.availWidth - window.outerWidth
+ )
+ );
+ const screenY = Math.max(
+ 0,
+ Math.min(
+ window.opener.screenY + Number(gLocation.getAttribute("offsetY")),
+ screen.availHeight - window.outerHeight
+ )
+ );
+ window.moveTo(screenX, screenY);
+ }
+}
+
+function SaveWindowLocation() {
+ if (gLocation) {
+ gLocation.setAttribute("offsetX", window.screenX - window.opener.screenX);
+ gLocation.setAttribute("offsetY", window.screenY - window.opener.screenY);
+ }
+}
+
+function onCancel() {
+ SaveWindowLocation();
+}
+
+function SetRelativeCheckbox(checkbox) {
+ if (!checkbox) {
+ checkbox = document.getElementById("MakeRelativeCheckbox");
+ if (!checkbox) {
+ return;
+ }
+ }
+
+ var editor = GetCurrentEditor();
+ // Mail never allows relative URLs, so hide the checkbox
+ if (editor && editor.flags & Ci.nsIEditor.eEditorMailMask) {
+ checkbox.collapsed = true;
+ return;
+ }
+
+ var input = document.getElementById(checkbox.getAttribute("for"));
+ if (!input) {
+ return;
+ }
+
+ var url = TrimString(input.value);
+ var urlScheme = GetScheme(url);
+
+ // Check it if url is relative (no scheme).
+ checkbox.checked = url.length > 0 && !urlScheme;
+
+ // Now do checkbox enabling:
+ var enable = false;
+
+ var docUrl = GetDocumentBaseUrl();
+ var docScheme = GetScheme(docUrl);
+
+ if (url && docUrl && docScheme) {
+ if (urlScheme) {
+ // Url is absolute
+ // If we can make a relative URL, then enable must be true!
+ // (this lets the smarts of MakeRelativeUrl do all the hard work)
+ enable = GetScheme(MakeRelativeUrl(url)).length == 0;
+ } else if (url[0] == "#") {
+ // Url is relative
+ // Check if url is a named anchor
+ // but document doesn't have a filename
+ // (it's probably "index.html" or "index.htm",
+ // but we don't want to allow a malformed URL)
+ var docFilename = GetFilename(docUrl);
+ enable = docFilename.length > 0;
+ } else {
+ // Any other url is assumed
+ // to be ok to try to make absolute
+ enable = true;
+ }
+ }
+
+ SetElementEnabled(checkbox, enable);
+}
+
+// oncommand handler for the Relativize checkbox in EditorOverlay.xhtml
+function MakeInputValueRelativeOrAbsolute(checkbox) {
+ var input = document.getElementById(checkbox.getAttribute("for"));
+ if (!input) {
+ return;
+ }
+
+ var docUrl = GetDocumentBaseUrl();
+ if (!docUrl) {
+ // Checkbox should be disabled if not saved,
+ // but keep this error message in case we change that
+ Services.prompt.alert(window, "", GetString("SaveToUseRelativeUrl"));
+ window.focus();
+ } else {
+ // Note that "checked" is opposite of its last state,
+ // which determines what we want to do here
+ if (checkbox.checked) {
+ input.value = MakeRelativeUrl(input.value);
+ } else {
+ input.value = MakeAbsoluteUrl(input.value);
+ }
+
+ // Reset checkbox to reflect url state
+ SetRelativeCheckbox(checkbox);
+ }
+}
+
+var IsBlockParent = [
+ "applet",
+ "blockquote",
+ "body",
+ "center",
+ "dd",
+ "div",
+ "form",
+ "li",
+ "noscript",
+ "object",
+ "td",
+ "th",
+];
+
+var NotAnInlineParent = [
+ "col",
+ "colgroup",
+ "dl",
+ "dir",
+ "menu",
+ "ol",
+ "table",
+ "tbody",
+ "tfoot",
+ "thead",
+ "tr",
+ "ul",
+];
+
+function FillLinkMenulist(linkMenulist, headingsArray) {
+ var editor = GetCurrentEditor();
+ try {
+ var treeWalker = editor.document.createTreeWalker(
+ editor.document,
+ 1,
+ null,
+ true
+ );
+ var headingList = [];
+ var anchorList = []; // for sorting
+ var anchorMap = {}; // for weeding out duplicates and making heading anchors unique
+ var anchor;
+ var i;
+ for (
+ var element = treeWalker.nextNode();
+ element;
+ element = treeWalker.nextNode()
+ ) {
+ // grab headings
+ // Skip headings that already have a named anchor as their first child
+ // (this may miss nearby anchors, but at least we don't insert another
+ // under the same heading)
+ if (
+ HTMLHeadingElement.isInstance(element) &&
+ element.textContent &&
+ !(
+ HTMLAnchorElement.isInstance(element.firstChild) &&
+ element.firstChild.name
+ )
+ ) {
+ headingList.push(element);
+ }
+
+ // grab named anchors
+ if (HTMLAnchorElement.isInstance(element) && element.name) {
+ anchor = "#" + element.name;
+ if (!(anchor in anchorMap)) {
+ anchorList.push({ anchor, sortkey: anchor.toLowerCase() });
+ anchorMap[anchor] = true;
+ }
+ }
+
+ // grab IDs
+ if (element.id) {
+ anchor = "#" + element.id;
+ if (!(anchor in anchorMap)) {
+ anchorList.push({ anchor, sortkey: anchor.toLowerCase() });
+ anchorMap[anchor] = true;
+ }
+ }
+ }
+ // add anchor for headings
+ for (i = 0; i < headingList.length; i++) {
+ var heading = headingList[i];
+
+ // Use just first 40 characters, don't add "...",
+ // and replace whitespace with "_" and strip non-word characters
+ anchor =
+ "#" +
+ ConvertToCDATAString(
+ TruncateStringAtWordEnd(heading.textContent, 40, false)
+ );
+
+ // Append "_" to any name already in the list
+ while (anchor in anchorMap) {
+ anchor += "_";
+ }
+ anchorList.push({ anchor, sortkey: anchor.toLowerCase() });
+ anchorMap[anchor] = true;
+
+ // Save nodes in an array so we can create anchor node under it later
+ headingsArray[anchor] = heading;
+ }
+ let menuItems = [];
+ if (anchorList.length) {
+ // case insensitive sort
+ anchorList.sort((a, b) => {
+ if (a.sortkey < b.sortkey) {
+ return -1;
+ }
+ if (a.sortkey > b.sortkey) {
+ return 1;
+ }
+ return 0;
+ });
+ for (i = 0; i < anchorList.length; i++) {
+ menuItems.push(createMenuItem(anchorList[i].anchor));
+ }
+ } else {
+ // Don't bother with named anchors in Mail.
+ if (editor && editor.flags & Ci.nsIEditor.eEditorMailMask) {
+ linkMenulist.removeAttribute("enablehistory");
+ return;
+ }
+ let item = createMenuItem(GetString("NoNamedAnchorsOrHeadings"));
+ item.setAttribute("disabled", "true");
+ menuItems.push(item);
+ }
+ window.addEventListener("contextmenu", event => {
+ if (document.getElementById("datalist-menuseparator")) {
+ return;
+ }
+ let menuseparator = document.createXULElement("menuseparator");
+ menuseparator.setAttribute("id", "datalist-menuseparator");
+ document.getElementById("textbox-contextmenu").appendChild(menuseparator);
+ for (let menuitem of menuItems) {
+ document.getElementById("textbox-contextmenu").appendChild(menuitem);
+ }
+ });
+ } catch (e) {}
+}
+
+function createMenuItem(label) {
+ var menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", label);
+ menuitem.addEventListener("click", event => {
+ gDialog.hrefInput.value = label;
+ ChangeLinkLocation();
+ });
+ return menuitem;
+}
+
+// Shared by Image and Link dialogs for the "Choose" button for links
+function chooseLinkFile() {
+ GetLocalFileURL("html, img").then(fileURL => {
+ // Always try to relativize local file URLs
+ if (gHaveDocumentUrl) {
+ fileURL = MakeRelativeUrl(fileURL);
+ }
+
+ gDialog.hrefInput.value = fileURL;
+
+ // Do stuff specific to a particular dialog
+ // (This is defined separately in Image and Link dialogs)
+ ChangeLinkLocation();
+ });
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdDictionary.js b/comm/mail/components/compose/content/dialogs/EdDictionary.js
new file mode 100644
index 0000000000..a79a01469c
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdDictionary.js
@@ -0,0 +1,138 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var gSpellChecker;
+var gWordToAdd;
+
+function Startup() {
+ if (!GetCurrentEditor()) {
+ window.close();
+ return;
+ }
+ // Get the SpellChecker shell
+ if ("gSpellChecker" in window.opener && window.opener.gSpellChecker) {
+ gSpellChecker = window.opener.gSpellChecker;
+ }
+
+ if (!gSpellChecker) {
+ dump("SpellChecker not found!!!\n");
+ window.close();
+ return;
+ }
+ // The word to add word is passed as the 2nd extra parameter in window.openDialog()
+ gWordToAdd = window.arguments[1];
+
+ gDialog.WordInput = document.getElementById("WordInput");
+ gDialog.DictionaryList = document.getElementById("DictionaryList");
+
+ gDialog.WordInput.value = gWordToAdd;
+ FillDictionaryList();
+
+ // Select the supplied word if it is already in the list
+ SelectWordToAddInList();
+ SetTextboxFocus(gDialog.WordInput);
+}
+
+function ValidateWordToAdd() {
+ gWordToAdd = TrimString(gDialog.WordInput.value);
+ if (gWordToAdd.length > 0) {
+ return true;
+ }
+ return false;
+}
+
+function SelectWordToAddInList() {
+ for (var i = 0; i < gDialog.DictionaryList.getRowCount(); i++) {
+ var wordInList = gDialog.DictionaryList.getItemAtIndex(i);
+ if (wordInList && gWordToAdd == wordInList.label) {
+ gDialog.DictionaryList.selectedIndex = i;
+ break;
+ }
+ }
+}
+
+function AddWord() {
+ if (ValidateWordToAdd()) {
+ try {
+ gSpellChecker.AddWordToDictionary(gWordToAdd);
+ } catch (e) {
+ dump(
+ "Exception occurred in gSpellChecker.AddWordToDictionary\nWord to add probably already existed\n"
+ );
+ }
+
+ // Rebuild the dialog list
+ FillDictionaryList();
+
+ SelectWordToAddInList();
+ gDialog.WordInput.value = "";
+ }
+}
+
+function RemoveWord() {
+ var selIndex = gDialog.DictionaryList.selectedIndex;
+ if (selIndex >= 0) {
+ var word = gDialog.DictionaryList.selectedItem.label;
+
+ // Remove word from list
+ gDialog.DictionaryList.selectedItem.remove();
+
+ // Remove from dictionary
+ try {
+ // Not working: BUG 43348
+ gSpellChecker.RemoveWordFromDictionary(word);
+ } catch (e) {
+ dump("Failed to remove word from dictionary\n");
+ }
+
+ ResetSelectedItem(selIndex);
+ }
+}
+
+function FillDictionaryList() {
+ var selIndex = gDialog.DictionaryList.selectedIndex;
+
+ // Clear the current contents of the list
+ ClearListbox(gDialog.DictionaryList);
+
+ // Get the list from the spell checker
+ gSpellChecker.GetPersonalDictionary();
+
+ var haveList = false;
+
+ // Get words until an empty string is returned
+ do {
+ var word = gSpellChecker.GetPersonalDictionaryWord();
+ if (word != "") {
+ gDialog.DictionaryList.appendItem(word, "");
+ haveList = true;
+ }
+ } while (word != "");
+
+ // XXX: BUG 74467: If list is empty, it doesn't layout to full height correctly
+ // (ignores "rows" attribute) (bug is latered, so we are fixing here for now)
+ if (!haveList) {
+ gDialog.DictionaryList.appendItem("", "");
+ }
+
+ ResetSelectedItem(selIndex);
+}
+
+function ResetSelectedItem(index) {
+ var lastIndex = gDialog.DictionaryList.getRowCount() - 1;
+ if (index > lastIndex) {
+ index = lastIndex;
+ }
+
+ // If we didn't have a selected item,
+ // set it to the first item
+ if (index == -1 && lastIndex >= 0) {
+ index = 0;
+ }
+
+ gDialog.DictionaryList.selectedIndex = index;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdDictionary.xhtml b/comm/mail/components/compose/content/dialogs/EdDictionary.xhtml
new file mode 100644
index 0000000000..c5c33212a9
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdDictionary.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorPersonalDictionary.dtd">
+<window
+ id="dictionaryDlg"
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ persist="screenX screenY"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog
+ buttonlabelaccept="&CloseButton.label;"
+ buttonaccesskeyaccept="&CloseButton.accessKey;"
+ buttons="accept"
+ >
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDictionary.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <hbox flex="1">
+ <div xmlns="http://www.w3.org/1999/xhtml" class="grid-two-column">
+ <div class="flex-items-center grid-item-span-row">
+ <xul:label
+ id="WordInputLabel"
+ value="&wordEditField.label;"
+ control="WordInput"
+ accesskey="&wordEditField.accessKey;"
+ />
+ </div>
+ <div>
+ <input
+ id="WordInput"
+ type="text"
+ style="width: 14.5em"
+ aria-labelledby="WordInputLabel"
+ />
+ </div>
+ <div>
+ <xul:button
+ id="AddWord"
+ oncommand="AddWord()"
+ label="&AddButton.label;"
+ accesskey="&AddButton.accessKey;"
+ />
+ </div>
+ <div class="flex-items-center grid-item-span-row">
+ <xul:label
+ value="&DictionaryList.label;"
+ control="DictionaryList"
+ accesskey="&DictionaryList.accessKey;"
+ />
+ </div>
+ <div>
+ <xul:richlistbox
+ id="DictionaryList"
+ style="width: 15em; height: 10em"
+ />
+ </div>
+ <div>
+ <xul:button
+ id="RemoveWord"
+ oncommand="RemoveWord()"
+ label="&RemoveButton.label;"
+ accesskey="&RemoveButton.accessKey;"
+ />
+ </div>
+ </div>
+ </hbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdHLineProps.js b/comm/mail/components/compose/content/dialogs/EdHLineProps.js
new file mode 100644
index 0000000000..4a5393d1dc
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdHLineProps.js
@@ -0,0 +1,227 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var tagName = "hr";
+var gHLineElement;
+var width;
+var height;
+var align;
+var shading;
+const gMaxHRSize = 1000; // This is hard-coded in nsHTMLHRElement::StringToAttribute()
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+ try {
+ // Get the selected horizontal line
+ gHLineElement = editor.getSelectedElement(tagName);
+ } catch (e) {}
+
+ if (!gHLineElement) {
+ // We should never be here if not editing an existing HLine
+ window.close();
+ return;
+ }
+ gDialog.heightInput = document.getElementById("height");
+ gDialog.widthInput = document.getElementById("width");
+ gDialog.leftAlign = document.getElementById("leftAlign");
+ gDialog.centerAlign = document.getElementById("centerAlign");
+ gDialog.rightAlign = document.getElementById("rightAlign");
+ gDialog.alignGroup = gDialog.rightAlign.radioGroup;
+ gDialog.shading = document.getElementById("3dShading");
+ gDialog.pixelOrPercentMenulist = document.getElementById(
+ "pixelOrPercentMenulist"
+ );
+
+ // Make a copy to use for AdvancedEdit and onSaveDefault
+ globalElement = gHLineElement.cloneNode(false);
+
+ // Initialize control values based on existing attributes
+ InitDialog();
+
+ // SET FOCUS TO FIRST CONTROL
+ SetTextboxFocus(gDialog.widthInput);
+
+ // Resize window
+ window.sizeToContent();
+
+ SetWindowLocation();
+}
+
+// Set dialog widgets with attribute data
+// We get them from globalElement copy so this can be used
+// by AdvancedEdit(), which is shared by all property dialogs
+function InitDialog() {
+ // Just to be confusing, "size" is used instead of height because it does
+ // not accept % values, only pixels
+ var height = GetHTMLOrCSSStyleValue(globalElement, "size", "height");
+ if (height.includes("px")) {
+ height = height.substr(0, height.indexOf("px"));
+ }
+ if (!height) {
+ height = 2; // Default value
+ }
+
+ // We will use "height" here and in UI
+ gDialog.heightInput.value = height;
+
+ // Get the width attribute of the element, stripping out "%"
+ // This sets contents of menulist (adds pixel and percent menuitems elements)
+ gDialog.widthInput.value = InitPixelOrPercentMenulist(
+ globalElement,
+ gHLineElement,
+ "width",
+ "pixelOrPercentMenulist"
+ );
+
+ var marginLeft = GetHTMLOrCSSStyleValue(
+ globalElement,
+ "align",
+ "margin-left"
+ ).toLowerCase();
+ var marginRight = GetHTMLOrCSSStyleValue(
+ globalElement,
+ "align",
+ "margin-right"
+ ).toLowerCase();
+ align = marginLeft + " " + marginRight;
+ gDialog.leftAlign.checked = align == "left left" || align == "0px auto";
+ gDialog.centerAlign.checked =
+ align == "center center" || align == "auto auto" || align == " ";
+ gDialog.rightAlign.checked = align == "right right" || align == "auto 0px";
+
+ if (gDialog.centerAlign.checked) {
+ gDialog.alignGroup.selectedItem = gDialog.centerAlign;
+ } else if (gDialog.rightAlign.checked) {
+ gDialog.alignGroup.selectedItem = gDialog.rightAlign;
+ } else {
+ gDialog.alignGroup.selectedItem = gDialog.leftAlign;
+ }
+
+ gDialog.shading.checked = !globalElement.hasAttribute("noshade");
+}
+
+function onSaveDefault() {
+ // "false" means set attributes on the globalElement,
+ // not the real element being edited
+ if (ValidateData()) {
+ var alignInt;
+ if (align == "left") {
+ alignInt = 0;
+ } else if (align == "right") {
+ alignInt = 2;
+ } else {
+ alignInt = 1;
+ }
+ Services.prefs.setIntPref("editor.hrule.align", alignInt);
+
+ var percent;
+ var widthInt;
+ var heightInt;
+
+ if (width) {
+ if (width.includes("%")) {
+ percent = true;
+ widthInt = Number(width.substr(0, width.indexOf("%")));
+ } else {
+ percent = false;
+ widthInt = Number(width);
+ }
+ } else {
+ percent = true;
+ widthInt = Number(100);
+ }
+
+ heightInt = height ? Number(height) : 2;
+
+ Services.prefs.setIntPref("editor.hrule.width", widthInt);
+ Services.prefs.setBoolPref("editor.hrule.width_percent", percent);
+ Services.prefs.setIntPref("editor.hrule.height", heightInt);
+ Services.prefs.setBoolPref("editor.hrule.shading", shading);
+
+ // Write the prefs out NOW!
+ Services.prefs.savePrefFile(null);
+ }
+}
+
+// Get and validate data from widgets.
+// Set attributes on globalElement so they can be accessed by AdvancedEdit()
+function ValidateData() {
+ // Height is always pixels
+ height = ValidateNumber(
+ gDialog.heightInput,
+ null,
+ 1,
+ gMaxHRSize,
+ globalElement,
+ "size",
+ false
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ width = ValidateNumber(
+ gDialog.widthInput,
+ gDialog.pixelOrPercentMenulist,
+ 1,
+ gMaxPixels,
+ globalElement,
+ "width",
+ false
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ align = "left";
+ if (gDialog.centerAlign.selected) {
+ // Don't write out default attribute
+ align = "";
+ } else if (gDialog.rightAlign.selected) {
+ align = "right";
+ }
+ if (align) {
+ globalElement.setAttribute("align", align);
+ } else {
+ try {
+ GetCurrentEditor().removeAttributeOrEquivalent(
+ globalElement,
+ "align",
+ true
+ );
+ } catch (e) {}
+ }
+
+ if (gDialog.shading.checked) {
+ shading = true;
+ globalElement.removeAttribute("noshade");
+ } else {
+ shading = false;
+ globalElement.setAttribute("noshade", "noshade");
+ }
+ return true;
+}
+
+function onAccept(event) {
+ if (ValidateData()) {
+ // Copy attributes from the globalElement to the document element
+ try {
+ GetCurrentEditor().cloneAttributes(gHLineElement, globalElement);
+ } catch (e) {}
+ return;
+ }
+ event.preventDefault();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml b/comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml
new file mode 100644
index 0000000000..21fa52147c
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % edHLineProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorHLineProperties.dtd">
+%edHLineProperties;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <!--- Element-specific methods -->
+ <script src="chrome://messenger/content/messengercompose/EdHLineProps.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <html:fieldset>
+ <html:legend>&dimensionsBox.label;</html:legend>
+ <html:table>
+ <html:tr>
+ <html:th>
+ <label
+ id="widthLabel"
+ control="width"
+ value="&widthEditField.label;"
+ accesskey="&widthEditField.accessKey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="width"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="widthLabel"
+ />
+ </html:td>
+ <html:td>
+ <menulist id="pixelOrPercentMenulist" />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="heightLabel"
+ control="height"
+ value="&heightEditField.label;"
+ accesskey="&heightEditField.accessKey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="height"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="heightLabel"
+ />
+ </html:td>
+ <html:td>
+ <label value="&pixelsPopup.value;" />
+ </html:td>
+ </html:tr>
+ </html:table>
+ <checkbox
+ id="3dShading"
+ label="&threeDShading.label;"
+ accesskey="&threeDShading.accessKey;"
+ />
+ </html:fieldset>
+ <html:fieldset>
+ <html:legend>&alignmentBox.label;</html:legend>
+ <radiogroup id="alignmentGroup" orient="horizontal">
+ <spacer class="spacer" />
+ <radio
+ id="leftAlign"
+ label="&leftRadio.label;"
+ accesskey="&leftRadio.accessKey;"
+ />
+ <radio
+ id="centerAlign"
+ label="&centerRadio.label;"
+ accesskey="&centerRadio.accessKey;"
+ />
+ <radio
+ id="rightAlign"
+ label="&rightRadio.label;"
+ accesskey="&rightRadio.accessKey;"
+ />
+ </radiogroup>
+ </html:fieldset>
+ <spacer class="spacer" />
+ <hbox>
+ <button
+ id="SaveDefault"
+ label="&saveSettings.label;"
+ accesskey="&saveSettings.accessKey;"
+ oncommand="onSaveDefault()"
+ tooltiptext="&saveSettings.tooltip;"
+ />
+ <spacer flex="1" />
+ <button
+ id="AdvancedEditButton"
+ oncommand="onAdvancedEdit();"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <separator class="groove" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdImageDialog.js b/comm/mail/components/compose/content/dialogs/EdImageDialog.js
new file mode 100644
index 0000000000..91e558cd50
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdImageDialog.js
@@ -0,0 +1,639 @@
+/* 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/. */
+
+/*
+ Note: We encourage non-empty alt text for images inserted into a page.
+ When there's no alt text, we always write 'alt=""' as the attribute, since "alt" is a required attribute.
+ We allow users to not have alt text by checking a "Don't use alterate text" radio button,
+ and we don't accept spaces as valid alt text. A space used to be required to avoid the error message
+ if user didn't enter alt text, but is unnecessary now that we no longer annoy the user
+ with the error dialog if alt="" is present on an img element.
+ We trim all spaces at the beginning and end of user's alt text
+*/
+
+/* import-globals-from ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var gInsertNewImage = true;
+var gDoAltTextError = false;
+var gConstrainOn = false;
+// Note used in current version, but these are set correctly
+// and could be used to reset width and height used for constrain ratio
+var gConstrainWidth = 0;
+var gConstrainHeight = 0;
+var imageElement;
+var gImageMap = 0;
+var gCanRemoveImageMap = false;
+var gRemoveImageMap = false;
+var gImageMapDisabled = false;
+var gActualWidth = "";
+var gActualHeight = "";
+var gOriginalSrc = "";
+var gTimerID;
+var gValidateTab;
+var gInsertNewIMap;
+
+// These must correspond to values in EditorDialog.css for each theme
+// (unfortunately, setting "style" attribute here doesn't work!)
+var gPreviewImageWidth = 80;
+var gPreviewImageHeight = 50;
+
+// dialog initialization code
+
+function ImageStartup() {
+ gDialog.tabBox = document.getElementById("TabBox");
+ gDialog.tabLocation = document.getElementById("imageLocationTab");
+ gDialog.tabDimensions = document.getElementById("imageDimensionsTab");
+ gDialog.tabBorder = document.getElementById("imageBorderTab");
+ gDialog.srcInput = document.getElementById("srcInput");
+ gDialog.titleInput = document.getElementById("titleInput");
+ gDialog.altTextInput = document.getElementById("altTextInput");
+ gDialog.altTextRadioGroup = document.getElementById("altTextRadioGroup");
+ gDialog.altTextRadio = document.getElementById("altTextRadio");
+ gDialog.noAltTextRadio = document.getElementById("noAltTextRadio");
+ gDialog.actualSizeRadio = document.getElementById("actualSizeRadio");
+ gDialog.constrainCheckbox = document.getElementById("constrainCheckbox");
+ gDialog.widthInput = document.getElementById("widthInput");
+ gDialog.heightInput = document.getElementById("heightInput");
+ gDialog.widthUnitsMenulist = document.getElementById("widthUnitsMenulist");
+ gDialog.heightUnitsMenulist = document.getElementById("heightUnitsMenulist");
+ gDialog.imagelrInput = document.getElementById("imageleftrightInput");
+ gDialog.imagetbInput = document.getElementById("imagetopbottomInput");
+ gDialog.border = document.getElementById("border");
+ gDialog.alignTypeSelect = document.getElementById("alignTypeSelect");
+ gDialog.PreviewWidth = document.getElementById("PreviewWidth");
+ gDialog.PreviewHeight = document.getElementById("PreviewHeight");
+ gDialog.PreviewImage = document.getElementById("preview-image");
+ gDialog.PreviewImage.addEventListener("load", PreviewImageLoaded);
+ gDialog.OkButton = document.querySelector("dialog").getButton("accept");
+}
+
+// Set dialog widgets with attribute data
+// We get them from globalElement copy so this can be used
+// by AdvancedEdit(), which is shared by all property dialogs
+function InitImage() {
+ // Set the controls to the image's attributes
+ var src = globalElement.getAttribute("src");
+
+ // For image insertion the 'src' attribute is null.
+ if (src) {
+ // Shorten data URIs for display.
+ shortenImageData(src, gDialog.srcInput);
+ }
+
+ // Set "Relativize" checkbox according to current URL state
+ SetRelativeCheckbox();
+
+ // Force loading of image from its source and show preview image
+ LoadPreviewImage();
+
+ gDialog.titleInput.value = globalElement.getAttribute("title");
+
+ var hasAltText = globalElement.hasAttribute("alt");
+ var altText = globalElement.getAttribute("alt");
+ gDialog.altTextInput.value = altText;
+ if (altText || (!hasAltText && globalElement.hasAttribute("src"))) {
+ gDialog.altTextRadioGroup.selectedItem = gDialog.altTextRadio;
+ } else if (hasAltText) {
+ gDialog.altTextRadioGroup.selectedItem = gDialog.noAltTextRadio;
+ }
+ SetAltTextDisabled(
+ gDialog.altTextRadioGroup.selectedItem == gDialog.noAltTextRadio
+ );
+
+ // setup the height and width widgets
+ var width = InitPixelOrPercentMenulist(
+ globalElement,
+ gInsertNewImage ? null : imageElement,
+ "width",
+ "widthUnitsMenulist",
+ gPixel
+ );
+ var height = InitPixelOrPercentMenulist(
+ globalElement,
+ gInsertNewImage ? null : imageElement,
+ "height",
+ "heightUnitsMenulist",
+ gPixel
+ );
+
+ // Set actual radio button if both set values are the same as actual
+ SetSizeWidgets(width, height);
+
+ gDialog.widthInput.value = gConstrainWidth = width || gActualWidth || "";
+ gDialog.heightInput.value = gConstrainHeight = height || gActualHeight || "";
+
+ // set spacing editfields
+ gDialog.imagelrInput.value = globalElement.getAttribute("hspace");
+ gDialog.imagetbInput.value = globalElement.getAttribute("vspace");
+
+ // dialog.border.value = globalElement.getAttribute("border");
+ var bv = GetHTMLOrCSSStyleValue(globalElement, "border", "border-top-width");
+ if (bv.includes("px")) {
+ // Strip out the px
+ bv = bv.substr(0, bv.indexOf("px"));
+ } else if (bv == "thin") {
+ bv = "1";
+ } else if (bv == "medium") {
+ bv = "3";
+ } else if (bv == "thick") {
+ bv = "5";
+ }
+ gDialog.border.value = bv;
+
+ // Get alignment setting
+ var align = globalElement.getAttribute("align");
+ if (align) {
+ align = align.toLowerCase();
+ }
+
+ switch (align) {
+ case "top":
+ case "middle":
+ case "right":
+ case "left":
+ gDialog.alignTypeSelect.value = align;
+ break;
+ default:
+ // Default or "bottom"
+ gDialog.alignTypeSelect.value = "bottom";
+ }
+
+ // Get image map for image
+ gImageMap = GetImageMap();
+
+ doOverallEnabling();
+ doDimensionEnabling();
+}
+
+function SetSizeWidgets(width, height) {
+ if (
+ !(width || height) ||
+ (gActualWidth &&
+ gActualHeight &&
+ width == gActualWidth &&
+ height == gActualHeight)
+ ) {
+ gDialog.actualSizeRadio.radioGroup.selectedItem = gDialog.actualSizeRadio;
+ }
+
+ if (!gDialog.actualSizeRadio.selected) {
+ // Decide if user's sizes are in the same ratio as actual sizes
+ if (gActualWidth && gActualHeight) {
+ if (gActualWidth > gActualHeight) {
+ gDialog.constrainCheckbox.checked =
+ Math.round((gActualHeight * width) / gActualWidth) == height;
+ } else {
+ gDialog.constrainCheckbox.checked =
+ Math.round((gActualWidth * height) / gActualHeight) == width;
+ }
+ }
+ }
+}
+
+// Disable alt text input when "Don't use alt" radio is checked
+function SetAltTextDisabled(disable) {
+ gDialog.altTextInput.disabled = disable;
+}
+
+function GetImageMap() {
+ var usemap = globalElement.getAttribute("usemap");
+ if (usemap) {
+ gCanRemoveImageMap = true;
+ let mapname = usemap.substr(1);
+ try {
+ return GetCurrentEditor().document.querySelector(
+ '[name="' + mapname + '"]'
+ );
+ } catch (e) {}
+ } else {
+ gCanRemoveImageMap = false;
+ }
+
+ return null;
+}
+
+function chooseFile() {
+ if (gTimerID) {
+ clearTimeout(gTimerID);
+ }
+
+ // Put focus into the input field
+ SetTextboxFocus(gDialog.srcInput);
+
+ GetLocalFileURL("img").then(fileURL => {
+ // Always try to relativize local file URLs
+ if (gHaveDocumentUrl) {
+ fileURL = MakeRelativeUrl(fileURL);
+ }
+
+ gDialog.srcInput.value = fileURL;
+
+ SetRelativeCheckbox();
+ doOverallEnabling();
+ LoadPreviewImage();
+ });
+}
+
+function PreviewImageLoaded() {
+ if (gDialog.PreviewImage) {
+ // Image loading has completed -- we can get actual width
+ gActualWidth = gDialog.PreviewImage.naturalWidth;
+ gActualHeight = gDialog.PreviewImage.naturalHeight;
+
+ if (gActualWidth && gActualHeight) {
+ // Use actual size or scale to fit preview if either dimension is too large
+ var width = gActualWidth;
+ var height = gActualHeight;
+ if (gActualWidth > gPreviewImageWidth) {
+ width = gPreviewImageWidth;
+ height = gActualHeight * (gPreviewImageWidth / gActualWidth);
+ }
+ if (height > gPreviewImageHeight) {
+ height = gPreviewImageHeight;
+ width = gActualWidth * (gPreviewImageHeight / gActualHeight);
+ }
+ gDialog.PreviewImage.width = width;
+ gDialog.PreviewImage.height = height;
+
+ gDialog.PreviewWidth.setAttribute("value", gActualWidth);
+ gDialog.PreviewHeight.setAttribute("value", gActualHeight);
+
+ document.getElementById("imagePreview").hidden = false;
+
+ SetSizeWidgets(gDialog.widthInput.value, gDialog.heightInput.value);
+ }
+
+ if (gDialog.actualSizeRadio.selected) {
+ SetActualSize();
+ }
+
+ window.sizeToContent();
+ }
+}
+
+function LoadPreviewImage() {
+ var imageSrc = gDialog.srcInput.value.trim();
+ if (!imageSrc) {
+ return;
+ }
+ if (isImageDataShortened(imageSrc)) {
+ imageSrc = restoredImageData(gDialog.srcInput);
+ }
+
+ try {
+ // Remove the image URL from image cache so it loads fresh
+ // (if we don't do this, loads after the first will always use image cache
+ // and we won't see image edit changes or be able to get actual width and height)
+
+ // We must have an absolute URL to preview it or remove it from the cache
+ imageSrc = MakeAbsoluteUrl(imageSrc);
+
+ if (GetScheme(imageSrc)) {
+ let uri = Services.io.newURI(imageSrc);
+ if (uri) {
+ let imgCache = Cc["@mozilla.org/image/cache;1"].getService(
+ Ci.imgICache
+ );
+
+ // This returns error if image wasn't in the cache; ignore that
+ imgCache.removeEntry(uri);
+ }
+ }
+ } catch (e) {}
+
+ gDialog.PreviewImage.addEventListener("load", PreviewImageLoaded, true);
+ gDialog.PreviewImage.src = imageSrc;
+}
+
+function SetActualSize() {
+ gDialog.widthInput.value = gActualWidth ? gActualWidth : "";
+ gDialog.widthUnitsMenulist.selectedIndex = 0;
+ gDialog.heightInput.value = gActualHeight ? gActualHeight : "";
+ gDialog.heightUnitsMenulist.selectedIndex = 0;
+ doDimensionEnabling();
+}
+
+function ChangeImageSrc() {
+ if (gTimerID) {
+ clearTimeout(gTimerID);
+ }
+
+ gTimerID = setTimeout(LoadPreviewImage, 800);
+
+ SetRelativeCheckbox();
+ doOverallEnabling();
+}
+
+function doDimensionEnabling() {
+ // Enabled unless "Actual Size" is selected
+ var enable = !gDialog.actualSizeRadio.selected;
+
+ // BUG 74145: After input field is disabled,
+ // setting it enabled causes blinking caret to appear
+ // even though focus isn't set to it.
+ SetElementEnabledById("heightInput", enable);
+ SetElementEnabledById("heightLabel", enable);
+ SetElementEnabledById("heightUnitsMenulist", enable);
+
+ SetElementEnabledById("widthInput", enable);
+ SetElementEnabledById("widthLabel", enable);
+ SetElementEnabledById("widthUnitsMenulist", enable);
+
+ var constrainEnable =
+ enable &&
+ gDialog.widthUnitsMenulist.selectedIndex == 0 &&
+ gDialog.heightUnitsMenulist.selectedIndex == 0;
+
+ SetElementEnabledById("constrainCheckbox", constrainEnable);
+}
+
+function doOverallEnabling() {
+ var enabled = TrimString(gDialog.srcInput.value) != "";
+
+ SetElementEnabled(gDialog.OkButton, enabled);
+ SetElementEnabledById("AdvancedEditButton1", enabled);
+ SetElementEnabledById("imagemapLabel", enabled);
+ SetElementEnabledById("removeImageMap", gCanRemoveImageMap);
+}
+
+function ToggleConstrain() {
+ // If just turned on, save the current width and height as basis for constrain ratio
+ // Thus clicking on/off lets user say "Use these values as aspect ration"
+ if (
+ gDialog.constrainCheckbox.checked &&
+ !gDialog.constrainCheckbox.disabled &&
+ gDialog.widthUnitsMenulist.selectedIndex == 0 &&
+ gDialog.heightUnitsMenulist.selectedIndex == 0
+ ) {
+ gConstrainWidth = Number(TrimString(gDialog.widthInput.value));
+ gConstrainHeight = Number(TrimString(gDialog.heightInput.value));
+ }
+}
+
+function constrainProportions(srcID, destID) {
+ var srcElement = document.getElementById(srcID);
+ if (!srcElement) {
+ return;
+ }
+
+ var destElement = document.getElementById(destID);
+ if (!destElement) {
+ return;
+ }
+
+ // always force an integer (whether we are constraining or not)
+ forceInteger(srcID);
+
+ if (
+ !gActualWidth ||
+ !gActualHeight ||
+ !(gDialog.constrainCheckbox.checked && !gDialog.constrainCheckbox.disabled)
+ ) {
+ return;
+ }
+
+ // double-check that neither width nor height is in percent mode; bail if so!
+ if (
+ gDialog.widthUnitsMenulist.selectedIndex != 0 ||
+ gDialog.heightUnitsMenulist.selectedIndex != 0
+ ) {
+ return;
+ }
+
+ // This always uses the actual width and height ratios
+ // which is kind of funky if you change one number without the constrain
+ // and then turn constrain on and change a number
+ // I prefer the old strategy (below) but I can see some merit to this solution
+ if (srcID == "widthInput") {
+ destElement.value = Math.round(
+ (srcElement.value * gActualHeight) / gActualWidth
+ );
+ } else {
+ destElement.value = Math.round(
+ (srcElement.value * gActualWidth) / gActualHeight
+ );
+ }
+
+ /*
+ // With this strategy, the width and height ratio
+ // can be reset to whatever the user entered.
+ if (srcID == "widthInput") {
+ destElement.value = Math.round( srcElement.value * gConstrainHeight / gConstrainWidth );
+ } else {
+ destElement.value = Math.round( srcElement.value * gConstrainWidth / gConstrainHeight );
+ }
+ */
+}
+
+function removeImageMap() {
+ gRemoveImageMap = true;
+ gCanRemoveImageMap = false;
+ SetElementEnabledById("removeImageMap", false);
+}
+
+function SwitchToValidatePanel() {
+ if (
+ gDialog.tabBox &&
+ gValidateTab &&
+ gDialog.tabBox.selectedTab != gValidateTab
+ ) {
+ gDialog.tabBox.selectedTab = gValidateTab;
+ }
+}
+
+// Get data from widgets, validate, and set for the global element
+// accessible to AdvancedEdit() [in EdDialogCommon.js]
+function ValidateImage() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ return false;
+ }
+
+ gValidateTab = gDialog.tabLocation;
+ if (!gDialog.srcInput.value) {
+ Services.prompt.alert(
+ window,
+ GetString("Alert"),
+ GetString("MissingImageError")
+ );
+ SwitchToValidatePanel();
+ gDialog.srcInput.focus();
+ return false;
+ }
+
+ // We must convert to "file:///" or "http://" format else image doesn't load!
+ let src = gDialog.srcInput.value.trim();
+
+ if (isImageDataShortened(src)) {
+ src = restoredImageData(gDialog.srcInput);
+ } else {
+ var checkbox = document.getElementById("MakeRelativeCheckbox");
+ try {
+ if (checkbox && !checkbox.checked) {
+ src = Services.uriFixup.createFixupURI(
+ src,
+ Ci.nsIURIFixup.FIXUP_FLAG_NONE
+ ).spec;
+ }
+ } catch (e) {}
+
+ globalElement.setAttribute("src", src);
+ }
+
+ let title = gDialog.titleInput.value.trim();
+ if (title) {
+ globalElement.setAttribute("title", title);
+ } else {
+ globalElement.removeAttribute("title");
+ }
+
+ // Force user to enter Alt text only if "Alternate text" radio is checked
+ // Don't allow just spaces in alt text
+ var alt = "";
+ var useAlt = gDialog.altTextRadioGroup.selectedItem == gDialog.altTextRadio;
+ if (useAlt) {
+ alt = TrimString(gDialog.altTextInput.value);
+ }
+
+ if (alt || !useAlt) {
+ globalElement.setAttribute("alt", alt);
+ } else if (!gDoAltTextError) {
+ globalElement.removeAttribute("alt");
+ } else {
+ Services.prompt.alert(window, GetString("Alert"), GetString("NoAltText"));
+ SwitchToValidatePanel();
+ gDialog.altTextInput.focus();
+ return false;
+ }
+
+ var width = "";
+ var height = "";
+
+ gValidateTab = gDialog.tabDimensions;
+ if (!gDialog.actualSizeRadio.selected) {
+ // Get user values for width and height
+ width = ValidateNumber(
+ gDialog.widthInput,
+ gDialog.widthUnitsMenulist,
+ 1,
+ gMaxPixels,
+ globalElement,
+ "width",
+ false,
+ true
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ height = ValidateNumber(
+ gDialog.heightInput,
+ gDialog.heightUnitsMenulist,
+ 1,
+ gMaxPixels,
+ globalElement,
+ "height",
+ false,
+ true
+ );
+ if (gValidationError) {
+ return false;
+ }
+ }
+
+ // We always set the width and height attributes, even if same as actual.
+ // This speeds up layout of pages since sizes are known before image is loaded
+ if (!width) {
+ width = gActualWidth;
+ }
+ if (!height) {
+ height = gActualHeight;
+ }
+
+ // Remove existing width and height only if source changed
+ // and we couldn't obtain actual dimensions
+ var srcChanged = src != gOriginalSrc;
+ if (width) {
+ editor.setAttributeOrEquivalent(globalElement, "width", width, true);
+ } else if (srcChanged) {
+ editor.removeAttributeOrEquivalent(globalElement, "width", true);
+ }
+
+ if (height) {
+ editor.setAttributeOrEquivalent(globalElement, "height", height, true);
+ } else if (srcChanged) {
+ editor.removeAttributeOrEquivalent(globalElement, "height", true);
+ }
+
+ // spacing attributes
+ gValidateTab = gDialog.tabBorder;
+ ValidateNumber(
+ gDialog.imagelrInput,
+ null,
+ 0,
+ gMaxPixels,
+ globalElement,
+ "hspace",
+ false,
+ true,
+ true
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ ValidateNumber(
+ gDialog.imagetbInput,
+ null,
+ 0,
+ gMaxPixels,
+ globalElement,
+ "vspace",
+ false,
+ true
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ // note this is deprecated and should be converted to stylesheets
+ ValidateNumber(
+ gDialog.border,
+ null,
+ 0,
+ gMaxPixels,
+ globalElement,
+ "border",
+ false,
+ true
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ // Default or setting "bottom" means don't set the attribute
+ // Note that the attributes "left" and "right" are opposite
+ // of what we use in the UI, which describes where the TEXT wraps,
+ // not the image location (which is what the HTML describes)
+ switch (gDialog.alignTypeSelect.value) {
+ case "top":
+ case "middle":
+ case "right":
+ case "left":
+ editor.setAttributeOrEquivalent(
+ globalElement,
+ "align",
+ gDialog.alignTypeSelect.value,
+ true
+ );
+ break;
+ default:
+ try {
+ editor.removeAttributeOrEquivalent(globalElement, "align", true);
+ } catch (e) {}
+ }
+
+ return true;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js b/comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js
new file mode 100644
index 0000000000..9c41679c15
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js
@@ -0,0 +1,144 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var gMsgCompProcessLink = false;
+var gMsgCompInputElement = null;
+var gMsgCompPrevInputValue = null;
+var gMsgCompPrevMozDoNotSendAttribute;
+var gMsgCompAttachSourceElement = null;
+
+function OnLoadDialog() {
+ gMsgCompAttachSourceElement = document.getElementById("AttachSourceToMail");
+ var editor = GetCurrentEditor();
+ if (
+ gMsgCompAttachSourceElement &&
+ editor &&
+ editor.flags & Ci.nsIEditor.eEditorMailMask
+ ) {
+ SetRelativeCheckbox = function () {
+ SetAttachCheckbox();
+ };
+ // initialize the AttachSourceToMail checkbox
+ gMsgCompAttachSourceElement.hidden = false;
+
+ switch (document.querySelector("dialog").id) {
+ case "imageDlg":
+ gMsgCompInputElement = gDialog.srcInput;
+ gMsgCompProcessLink = false;
+ break;
+ case "linkDlg":
+ gMsgCompInputElement = gDialog.hrefInput;
+ gMsgCompProcessLink = true;
+ break;
+ }
+ if (gMsgCompInputElement) {
+ SetAttachCheckbox();
+ gMsgCompPrevMozDoNotSendAttribute =
+ globalElement.getAttribute("moz-do-not-send");
+ }
+ }
+}
+addEventListener("load", OnLoadDialog, false);
+
+function OnAcceptDialog() {
+ // Auto-convert file URLs to data URLs. If we're in the link properties
+ // dialog convert only when requested - for the image dialog do it always.
+ if (
+ /^file:/i.test(gMsgCompInputElement.value.trim()) &&
+ (gMsgCompAttachSourceElement.checked || !gMsgCompProcessLink)
+ ) {
+ var dataURI = GenerateDataURL(gMsgCompInputElement.value.trim());
+ gMsgCompInputElement.value = dataURI;
+ gMsgCompAttachSourceElement.checked = true;
+ }
+ DoAttachSourceCheckbox();
+}
+document.addEventListener("dialogaccept", OnAcceptDialog, true);
+
+function SetAttachCheckbox() {
+ var resetCheckbox = false;
+ var mozDoNotSend = globalElement.getAttribute("moz-do-not-send");
+
+ // In case somebody played with the advanced property and changed the moz-do-not-send attribute
+ if (mozDoNotSend != gMsgCompPrevMozDoNotSendAttribute) {
+ gMsgCompPrevMozDoNotSendAttribute = mozDoNotSend;
+ resetCheckbox = true;
+ }
+
+ // Has the URL changed
+ if (
+ gMsgCompInputElement &&
+ gMsgCompInputElement.value != gMsgCompPrevInputValue
+ ) {
+ gMsgCompPrevInputValue = gMsgCompInputElement.value;
+ resetCheckbox = true;
+ }
+
+ if (gMsgCompInputElement && resetCheckbox) {
+ // Here is the rule about how to set the checkbox Attach Source To Message:
+ // If the attribute "moz-do-not-send" has not been set, we look at the scheme of the URL
+ // and at some preference to decide what is the best for the user.
+ // If it is set to "false", the checkbox is checked, otherwise unchecked.
+ var attach = false;
+ if (mozDoNotSend == null) {
+ // We haven't yet set the "moz-do-not-send" attribute.
+ var inputValue = gMsgCompInputElement.value.trim();
+ if (/^(file|data):/i.test(inputValue)) {
+ // For files or data URLs, default to attach them.
+ attach = true;
+ } else if (
+ !gMsgCompProcessLink && // Implies image dialogue.
+ /^https?:/i.test(inputValue)
+ ) {
+ // For images loaded via http(s) we default to the preference value.
+ attach = Services.prefs.getBoolPref("mail.compose.attach_http_images");
+ }
+ } else {
+ attach = mozDoNotSend == "false";
+ }
+
+ gMsgCompAttachSourceElement.checked = attach;
+ }
+}
+
+function DoAttachSourceCheckbox() {
+ gMsgCompPrevMozDoNotSendAttribute =
+ (!gMsgCompAttachSourceElement.checked).toString();
+ globalElement.setAttribute(
+ "moz-do-not-send",
+ gMsgCompPrevMozDoNotSendAttribute
+ );
+}
+
+function GenerateDataURL(url) {
+ var file = Services.io.newURI(url).QueryInterface(Ci.nsIFileURL).file;
+ var contentType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromFile(file);
+ var inputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ inputStream.init(file, 0x01, 0o600, 0);
+ var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ stream.setInputStream(inputStream);
+ let data = "";
+ while (stream.available() > 0) {
+ data += stream.readBytes(stream.available());
+ }
+ let encoded = btoa(data);
+ stream.close();
+ return (
+ "data:" +
+ contentType +
+ ";filename=" +
+ encodeURIComponent(file.leafName) +
+ ";base64," +
+ encoded
+ );
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdImageProps.js b/comm/mail/components/compose/content/dialogs/EdImageProps.js
new file mode 100644
index 0000000000..861d098edc
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdImageProps.js
@@ -0,0 +1,293 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+/* import-globals-from EdImageDialog.js */
+
+var gAnchorElement = null;
+var gLinkElement = null;
+var gOriginalHref = "";
+var gHNodeArray = {};
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+
+ ImageStartup();
+ gDialog.hrefInput = document.getElementById("hrefInput");
+ gDialog.makeRelativeLink = document.getElementById("MakeRelativeLink");
+ gDialog.showLinkBorder = document.getElementById("showLinkBorder");
+ gDialog.linkTab = document.getElementById("imageLinkTab");
+ gDialog.linkAdvanced = document.getElementById("LinkAdvancedEditButton");
+
+ // Get a single selected image element
+ var tagName = "img";
+ if ("arguments" in window && window.arguments[0]) {
+ imageElement = window.arguments[0];
+ // We've been called from form field properties, so we can't insert a link
+ gDialog.linkTab.remove();
+ gDialog.linkTab = null;
+ } else {
+ // First check for <input type="image">
+ try {
+ imageElement = editor.getSelectedElement("input");
+
+ if (!imageElement || imageElement.getAttribute("type") != "image") {
+ // Get a single selected image element
+ imageElement = editor.getSelectedElement(tagName);
+ if (imageElement) {
+ gAnchorElement = editor.getElementOrParentByTagName(
+ "href",
+ imageElement
+ );
+ }
+ }
+ } catch (e) {}
+ }
+
+ if (imageElement) {
+ // We found an element and don't need to insert one
+ if (imageElement.hasAttribute("src")) {
+ gInsertNewImage = false;
+ gActualWidth = imageElement.naturalWidth;
+ gActualHeight = imageElement.naturalHeight;
+ }
+ } else {
+ gInsertNewImage = true;
+
+ // We don't have an element selected,
+ // so create one with default attributes
+ try {
+ imageElement = editor.createElementWithDefaults(tagName);
+ } catch (e) {}
+
+ if (!imageElement) {
+ dump("Failed to get selected element or create a new one!\n");
+ window.close();
+ return;
+ }
+ try {
+ gAnchorElement = editor.getSelectedElement("href");
+ } catch (e) {}
+ }
+
+ // Make a copy to use for AdvancedEdit
+ globalElement = imageElement.cloneNode(false);
+
+ // We only need to test for this once per dialog load
+ gHaveDocumentUrl = GetDocumentBaseUrl();
+
+ InitDialog();
+ if (gAnchorElement) {
+ gOriginalHref = gAnchorElement.getAttribute("href");
+ // Make a copy to use for AdvancedEdit
+ gLinkElement = gAnchorElement.cloneNode(false);
+ } else {
+ gLinkElement = editor.createElementWithDefaults("a");
+ }
+ gDialog.hrefInput.value = gOriginalHref;
+
+ FillLinkMenulist(gDialog.hrefInput, gHNodeArray);
+ ChangeLinkLocation();
+
+ // Save initial source URL
+ gOriginalSrc = gDialog.srcInput.value;
+
+ // By default turn constrain on, but both width and height must be in pixels
+ gDialog.constrainCheckbox.checked =
+ gDialog.widthUnitsMenulist.selectedIndex == 0 &&
+ gDialog.heightUnitsMenulist.selectedIndex == 0;
+
+ // Start in "Link" tab if 2nd argument is true
+ if (gDialog.linkTab && "arguments" in window && window.arguments[1]) {
+ document.getElementById("TabBox").selectedTab = gDialog.linkTab;
+ SetTextboxFocus(gDialog.hrefInput);
+ } else {
+ SetTextboxFocus(gDialog.srcInput);
+ }
+
+ SetWindowLocation();
+}
+
+// Set dialog widgets with attribute data
+// We get them from globalElement copy so this can be used
+// by AdvancedEdit(), which is shared by all property dialogs
+function InitDialog() {
+ InitImage();
+ var border = TrimString(gDialog.border.value);
+ gDialog.showLinkBorder.checked = border != "" && border > 0;
+}
+
+function ChangeLinkLocation() {
+ var href = TrimString(gDialog.hrefInput.value);
+ SetRelativeCheckbox(gDialog.makeRelativeLink);
+ gDialog.showLinkBorder.disabled = !href;
+ gDialog.linkAdvanced.disabled = !href;
+ gLinkElement.setAttribute("href", href);
+}
+
+function ToggleShowLinkBorder() {
+ if (gDialog.showLinkBorder.checked) {
+ var border = TrimString(gDialog.border.value);
+ if (!border || border == "0") {
+ gDialog.border.value = "2";
+ }
+ } else {
+ gDialog.border.value = "0";
+ }
+}
+
+// Get data from widgets, validate, and set for the global element
+// accessible to AdvancedEdit() [in EdDialogCommon.js]
+function ValidateData() {
+ return ValidateImage();
+}
+
+function onAccept(event) {
+ // Use this now (default = false) so Advanced Edit button dialog doesn't trigger error message
+ gDoAltTextError = true;
+ window.opener.gMsgCompose.allowRemoteContent = true;
+ if (ValidateData()) {
+ if ("arguments" in window && window.arguments[0]) {
+ SaveWindowLocation();
+ return;
+ }
+
+ var editor = GetCurrentEditor();
+
+ editor.beginTransaction();
+
+ try {
+ if (gRemoveImageMap) {
+ globalElement.removeAttribute("usemap");
+ if (gImageMap) {
+ editor.deleteNode(gImageMap);
+ gInsertNewIMap = true;
+ gImageMap = null;
+ }
+ } else if (gImageMap) {
+ // un-comment to see that inserting image maps does not work!
+ /*
+ gImageMap = editor.createElementWithDefaults("map");
+ gImageMap.setAttribute("name", "testing");
+ var testArea = editor.createElementWithDefaults("area");
+ testArea.setAttribute("shape", "circle");
+ testArea.setAttribute("coords", "86,102,52");
+ testArea.setAttribute("href", "test");
+ gImageMap.appendChild(testArea);
+ */
+
+ // Assign to map if there is one
+ var mapName = gImageMap.getAttribute("name");
+ if (mapName != "") {
+ globalElement.setAttribute("usemap", "#" + mapName);
+ if (globalElement.getAttribute("border") == "") {
+ globalElement.setAttribute("border", 0);
+ }
+ }
+ }
+
+ // Create or remove the link as appropriate
+ var href = gDialog.hrefInput.value;
+ if (href != gOriginalHref) {
+ if (href && !gInsertNewImage) {
+ EditorSetTextProperty("a", "href", href);
+ // gAnchorElement is needed for cloning attributes later.
+ if (!gAnchorElement) {
+ gAnchorElement = editor.getElementOrParentByTagName(
+ "href",
+ imageElement
+ );
+ }
+ } else {
+ EditorRemoveTextProperty("href", "");
+ }
+ }
+
+ // If inside a link, always write the 'border' attribute
+ if (href) {
+ if (gDialog.showLinkBorder.checked) {
+ // Use default = 2 if border attribute is empty
+ if (!globalElement.hasAttribute("border")) {
+ globalElement.setAttribute("border", "2");
+ }
+ } else {
+ globalElement.setAttribute("border", "0");
+ }
+ }
+
+ if (gInsertNewImage) {
+ if (href) {
+ gLinkElement.appendChild(imageElement);
+ editor.insertElementAtSelection(gLinkElement, true);
+ } else {
+ // 'true' means delete the selection before inserting
+ editor.insertElementAtSelection(imageElement, true);
+ }
+ }
+
+ // Check to see if the link was to a heading
+ // Do this last because it moves the caret (BAD!)
+ if (href in gHNodeArray) {
+ var anchorNode = editor.createElementWithDefaults("a");
+ if (anchorNode) {
+ anchorNode.name = href.substr(1);
+ // Remember to use editor method so it is undoable!
+ editor.insertNode(anchorNode, gHNodeArray[href], 0);
+ }
+ }
+ // All values are valid - copy to actual element in doc or
+ // element we just inserted
+ editor.cloneAttributes(imageElement, globalElement);
+ if (gAnchorElement) {
+ editor.cloneAttributes(gAnchorElement, gLinkElement);
+ }
+
+ // If document is empty, the map element won't insert,
+ // so always insert the image first
+ if (gImageMap && gInsertNewIMap) {
+ // Insert the ImageMap element at beginning of document
+ var body = editor.rootElement;
+ editor.setShouldTxnSetSelection(false);
+ editor.insertNode(gImageMap, body, 0);
+ editor.setShouldTxnSetSelection(true);
+ }
+ } catch (e) {
+ dump(e);
+ }
+
+ editor.endTransaction();
+
+ SaveWindowLocation();
+ return;
+ }
+
+ gDoAltTextError = false;
+
+ event.preventDefault();
+}
+
+function onLinkAdvancedEdit() {
+ window.AdvancedEditOK = false;
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdAdvancedEdit.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal,resizable=yes",
+ "",
+ gLinkElement
+ );
+ window.focus();
+ if (window.AdvancedEditOK) {
+ gDialog.hrefInput.value = gLinkElement.getAttribute("href");
+ }
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdImageProps.xhtml b/comm/mail/components/compose/content/dialogs/EdImageProps.xhtml
new file mode 100644
index 0000000000..c894a30175
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdImageProps.xhtml
@@ -0,0 +1,454 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % edImageProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorImageProperties.dtd">
+%edImageProperties;
+<!ENTITY % composeEditorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/mailComposeEditorOverlay.dtd">
+%composeEditorOverlayDTD;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<!-- dialog containing a control requiring initial setup -->
+<window
+ windowtype="Mail:image"
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="min-height: 24em"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog id="imageDlg" buttons="accept,cancel" style="width: 68ch">
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdImageProps.js" />
+ <script src="chrome://messenger/content/messengercompose/EdImageDialog.js" />
+ <script src="chrome://messenger/content/messengercompose/EdImageLinkLoader.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <tabbox id="TabBox">
+ <tabs flex="1">
+ <tab id="imageLocationTab" label="&imageLocationTab.label;" />
+ <tab id="imageDimensionsTab" label="&imageDimensionsTab.label;" />
+ <tab id="imageAppearanceTab" label="&imageAppearanceTab.label;" />
+ <tab id="imageLinkTab" label="&imageLinkTab.label;" />
+ </tabs>
+ <tabpanels>
+ <vbox id="imageLocation">
+ <spacer class="spacer" />
+ <label
+ id="srcLabel"
+ control="srcInput"
+ value="&locationEditField.label;"
+ accesskey="&locationEditField.accessKey;"
+ tooltiptext="&locationEditField.tooltip;"
+ />
+ <tooltip id="shortenedDataURI">
+ <label value="&locationEditField.shortenedDataURI;" />
+ </tooltip>
+ <html:input
+ id="srcInput"
+ type="text"
+ oninput="ChangeImageSrc();"
+ tabindex="1"
+ class="uri-element input-inline"
+ title="&locationEditField.tooltip;"
+ aria-labelledby="srcLabel"
+ />
+ <hbox id="MakeRelativeHbox">
+ <checkbox
+ id="MakeRelativeCheckbox"
+ tabindex="2"
+ label="&makeUrlRelative.label;"
+ accesskey="&makeUrlRelative.accessKey;"
+ oncommand="MakeInputValueRelativeOrAbsolute(this);"
+ tooltiptext="&makeUrlRelative.tooltip;"
+ />
+ <checkbox
+ id="AttachSourceToMail"
+ hidden="true"
+ label="&attachImageSource.label;"
+ accesskey="&attachImageSource.accesskey;"
+ oncommand="DoAttachSourceCheckbox()"
+ />
+ <spacer flex="1" />
+ <button
+ id="ChooseFile"
+ tabindex="3"
+ oncommand="chooseFile()"
+ label="&chooseFileButton.label;"
+ accesskey="&chooseFileButton.accessKey;"
+ />
+ </hbox>
+ <spacer class="spacer" />
+ <radiogroup id="altTextRadioGroup" flex="1">
+ <hbox>
+ <vbox>
+ <hbox align="center" flex="1">
+ <label
+ id="titleLabel"
+ style="margin-left: 26px"
+ control="titleInput"
+ accesskey="&title.accessKey;"
+ value="&title.label;"
+ tooltiptext="&title.tooltip;"
+ />
+ </hbox>
+ <hbox align="center" flex="1">
+ <radio
+ id="altTextRadio"
+ value="usealt-yes"
+ label="&altText.label;"
+ accesskey="&altText.accessKey;"
+ tooltiptext="&altTextEditField.tooltip;"
+ persist="selected"
+ oncommand="SetAltTextDisabled(false);"
+ tabindex="5"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <html:input
+ id="titleInput"
+ type="text"
+ class="MinWidth20em input-inline"
+ title="&title.tooltip;"
+ tabindex="4"
+ aria-labelledby="titleLabel"
+ />
+ <html:input
+ id="altTextInput"
+ type="text"
+ class="MinWidth20em input-inline"
+ title="&altTextEditField.tooltip;"
+ oninput="SetAltTextDisabled(false);"
+ tabindex="6"
+ aria-labelledby="altTextRadio"
+ />
+ </vbox>
+ </hbox>
+ <radio
+ id="noAltTextRadio"
+ value="usealt-no"
+ label="&noAltText.label;"
+ accesskey="&noAltText.accessKey;"
+ persist="selected"
+ oncommand="SetAltTextDisabled(true);"
+ />
+ </radiogroup>
+ </vbox>
+
+ <vbox id="imageDimensions" align="start">
+ <spacer class="spacer" />
+ <hbox>
+ <radiogroup id="imgSizeGroup">
+ <radio
+ id="actualSizeRadio"
+ label="&actualSizeRadio.label;"
+ accesskey="&actualSizeRadio.accessKey;"
+ tooltiptext="&actualSizeRadio.tooltip;"
+ oncommand="SetActualSize()"
+ value="actual"
+ />
+ <radio
+ id="customSizeRadio"
+ label="&customSizeRadio.label;"
+ selected="true"
+ accesskey="&customSizeRadio.accessKey;"
+ tooltiptext="&customSizeRadio.tooltip;"
+ oncommand="doDimensionEnabling();"
+ value="custom"
+ />
+ </radiogroup>
+ <spacer flex="1" />
+ <vbox>
+ <spacer flex="1" />
+ <checkbox
+ id="constrainCheckbox"
+ label="&constrainCheckbox.label;"
+ accesskey="&constrainCheckbox.accessKey;"
+ oncommand="ToggleConstrain()"
+ tooltiptext="&constrainCheckbox.tooltip;"
+ />
+ </vbox>
+ <spacer flex="1" />
+ </hbox>
+ <spacer class="spacer" />
+ <hbox class="indent">
+ <html:table>
+ <html:tr>
+ <html:th>
+ <label
+ id="widthLabel"
+ control="widthInput"
+ accesskey="&widthEditField.accessKey;"
+ value="&widthEditField.label;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="widthInput"
+ type="number"
+ min="0"
+ class="narrow input-inline"
+ oninput="constrainProportions(this.id,'heightInput')"
+ aria-labelledby="widthLabel"
+ />
+ </html:td>
+ <html:td>
+ <menulist
+ id="widthUnitsMenulist"
+ oncommand="doDimensionEnabling();"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="heightLabel"
+ control="heightInput"
+ accesskey="&heightEditField.accessKey;"
+ value="&heightEditField.label;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="heightInput"
+ type="number"
+ min="0"
+ class="narrow input-inline"
+ oninput="constrainProportions(this.id,'widthInput')"
+ aria-labelledby="heightLabel"
+ />
+ </html:td>
+ <html:td>
+ <menulist
+ id="heightUnitsMenulist"
+ oncommand="doDimensionEnabling();"
+ />
+ </html:td>
+ </html:tr>
+ </html:table>
+ </hbox>
+ <spacer flex="1" />
+ </vbox>
+
+ <vbox id="imageAppearance">
+ <html:legend id="spacingLabel">&spacingBox.label;</html:legend>
+ <html:table>
+ <html:tr>
+ <html:th>
+ <label
+ id="leftrightLabel"
+ class="align-right"
+ control="imageleftrightInput"
+ accesskey="&leftRightEditField.accessKey;"
+ value="&leftRightEditField.label;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="imageleftrightInput"
+ type="number"
+ min="0"
+ class="narrow input-inline"
+ aria-labelledby="leftrightLabel"
+ />
+ </html:td>
+ <html:td id="leftrighttypeLabel"> &pixelsPopup.value; </html:td>
+ <html:td style="width: 80%">
+ <spacer />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="topbottomLabel"
+ class="align-right"
+ control="imagetopbottomInput"
+ accesskey="&topBottomEditField.accessKey;"
+ value="&topBottomEditField.label;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="imagetopbottomInput"
+ type="number"
+ min="0"
+ class="narrow input-inline"
+ aria-labelledby="topbottomLabel"
+ />
+ </html:td>
+ <html:td id="topbottomtypeLabel"> &pixelsPopup.value; </html:td>
+ <html:td>
+ <spacer />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="borderLabel"
+ class="align-right"
+ control="border"
+ accesskey="&borderEditField.accessKey;"
+ value="&borderEditField.label;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="border"
+ type="number"
+ min="0"
+ class="narrow input-inline"
+ aria-labelledby="borderLabel"
+ />
+ </html:td>
+ <html:td id="bordertypeLabel"> &pixelsPopup.value; </html:td>
+ <html:td>
+ <spacer />
+ </html:td>
+ </html:tr>
+ </html:table>
+ <separator class="thin" />
+ <html:legend id="alignLabel">&alignment.label;</html:legend>
+ <menulist id="alignTypeSelect" class="align-menu">
+ <menupopup>
+ <menuitem
+ class="align-menu menuitem-iconic"
+ value="top"
+ label="&topPopup.value;"
+ />
+ <menuitem
+ class="align-menu menuitem-iconic"
+ value="middle"
+ label="&centerPopup.value;"
+ />
+ <menuitem
+ class="align-menu menuitem-iconic"
+ value="bottom"
+ label="&bottomPopup.value;"
+ />
+ <!-- HTML attribute value is opposite of the button label on purpose -->
+ <menuitem
+ class="align-menu menuitem-iconic"
+ value="right"
+ label="&wrapLeftPopup.value;"
+ />
+ <menuitem
+ class="align-menu menuitem-iconic"
+ value="left"
+ label="&wrapRightPopup.value;"
+ />
+ </menupopup>
+ </menulist>
+ <separator class="thin" />
+ <html:legend id="imagemapLabel">&imagemapBox.label;</html:legend>
+ <html:div class="grid-two-column-equalsize">
+ <button
+ id="removeImageMap"
+ oncommand="removeImageMap()"
+ accesskey="&removeImageMapButton.accessKey;"
+ label="&removeImageMapButton.label;"
+ />
+ <spacer /><!-- remove when we restore Image Map Editor -->
+ </html:div>
+ </vbox>
+ <vbox>
+ <spacer class="spacer" />
+ <vbox id="LinkLocationBox">
+ <label
+ id="hrefLabel"
+ control="hrefInput"
+ accesskey="&LinkURLEditField2.accessKey;"
+ width="1"
+ >&LinkURLEditField2.label;</label
+ >
+ <html:input
+ id="hrefInput"
+ type="text"
+ class="uri-element padded input-inline"
+ oninput="ChangeLinkLocation();"
+ aria-labelledby="hrefLabel"
+ />
+ <hbox align="center">
+ <checkbox
+ id="MakeRelativeLink"
+ for="hrefInput"
+ label="&makeUrlRelative.label;"
+ accesskey="&makeUrlRelative.accessKey;"
+ oncommand="MakeInputValueRelativeOrAbsolute(this);"
+ tooltiptext="&makeUrlRelative.tooltip;"
+ />
+ <spacer flex="1" />
+ <button
+ label="&chooseFileLinkButton.label;"
+ accesskey="&chooseFileLinkButton.accessKey;"
+ oncommand="chooseLinkFile();"
+ />
+ </hbox>
+ </vbox>
+ <spacer class="spacer" />
+ <hbox>
+ <checkbox
+ id="showLinkBorder"
+ label="&showImageLinkBorder.label;"
+ accesskey="&showImageLinkBorder.accessKey;"
+ oncommand="ToggleShowLinkBorder();"
+ />
+ <spacer flex="1" />
+ </hbox>
+ <separator class="thin" />
+ <hbox pack="end">
+ <button
+ id="LinkAdvancedEditButton"
+ label="&LinkAdvancedEditButton.label;"
+ accesskey="&LinkAdvancedEditButton.accessKey;"
+ tooltiptext="&LinkAdvancedEditButton.tooltip;"
+ oncommand="onLinkAdvancedEdit();"
+ />
+ </hbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+
+ <spacer flex="1" />
+
+ <html:fieldset id="imagePreview" hidden="hidden">
+ <html:legend>&previewBox.label;</html:legend>
+
+ <html:figure>
+ <html:img id="preview-image" style="display: inline-block" alt="" />
+ <html:figcaption style="float: right">
+ <label value="&actualSize.label;" />
+ <label id="PreviewWidth" />x<label id="PreviewHeight" />
+ </html:figcaption>
+ </html:figure>
+ </html:fieldset>
+
+ <hbox pack="end">
+ <button
+ id="AdvancedEditButton1"
+ oncommand="onAdvancedEdit()"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+
+ <separator class="groove" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdInsSrc.js b/comm/mail/components/compose/content/dialogs/EdInsSrc.js
new file mode 100644
index 0000000000..d00f119ed7
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsSrc.js
@@ -0,0 +1,162 @@
+/* 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/. */
+
+/* Insert Source HTML dialog */
+
+/* import-globals-from ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var gFullDataStrings = new Map();
+var gShortDataStrings = new Map();
+var gListenerAttached = false;
+
+window.addEventListener("load", Startup);
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ let editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+
+ document
+ .querySelector("dialog")
+ .getButton("accept")
+ .removeAttribute("default");
+
+ // Create dialog object to store controls for easy access
+ gDialog.srcInput = document.getElementById("srcInput");
+
+ // Attach a paste listener so we can detect pasted data URIs we need to shorten.
+ gDialog.srcInput.addEventListener("paste", onPaste);
+
+ let selection;
+ try {
+ selection = editor.outputToString(
+ "text/html",
+ kOutputFormatted | kOutputSelectionOnly | kOutputWrap
+ );
+ } catch (e) {}
+ if (selection) {
+ selection = selection.replace(/<body[^>]*>/, "").replace(/<\/body>/, "");
+
+ // Shorten data URIs for display.
+ selection = replaceDataURIs(selection);
+
+ if (selection) {
+ gDialog.srcInput.value = selection;
+ }
+ }
+ // Set initial focus
+ gDialog.srcInput.focus();
+ SetWindowLocation();
+}
+
+function replaceDataURIs(input) {
+ return input.replace(
+ /(data:.+;base64,)([^"' >]+)/gi,
+ function (match, nonDataPart, dataPart) {
+ if (gShortDataStrings.has(dataPart)) {
+ // We found the exact same data URI, just return the shortened URI.
+ return nonDataPart + gShortDataStrings.get(dataPart);
+ }
+
+ let l = 5;
+ let key;
+ // Normally we insert the ellipsis after five characters but if it's not unique
+ // we include more data.
+ do {
+ key =
+ dataPart.substr(0, l) + "…" + dataPart.substr(dataPart.length - 10);
+ l++;
+ } while (gFullDataStrings.has(key) && l < dataPart.length - 10);
+ gFullDataStrings.set(key, dataPart);
+ gShortDataStrings.set(dataPart, key);
+
+ // Attach listeners. In case anyone copies/cuts from the HTML window,
+ // we want to restore the data URI on the clipboard.
+ if (!gListenerAttached) {
+ gDialog.srcInput.addEventListener("copy", onCopyOrCut);
+ gDialog.srcInput.addEventListener("cut", onCopyOrCut);
+ gListenerAttached = true;
+ }
+
+ return nonDataPart + key;
+ }
+ );
+}
+
+function onCopyOrCut(event) {
+ let startPos = gDialog.srcInput.selectionStart;
+ if (startPos == undefined) {
+ return;
+ }
+ let endPos = gDialog.srcInput.selectionEnd;
+ let clipboard = gDialog.srcInput.value.substring(startPos, endPos);
+
+ // Add back the original data URIs we stashed away earlier.
+ clipboard = clipboard.replace(
+ /(data:.+;base64,)([^"' >]+)/gi,
+ function (match, nonDataPart, key) {
+ if (!gFullDataStrings.has(key)) {
+ // User changed data URI.
+ return match;
+ }
+ return nonDataPart + gFullDataStrings.get(key);
+ }
+ );
+ event.clipboardData.setData("text/plain", clipboard);
+ if (event.type == "cut") {
+ // We have to cut the selection manually.
+ gDialog.srcInput.value =
+ gDialog.srcInput.value.substr(0, startPos) +
+ gDialog.srcInput.value.substr(endPos);
+ }
+ event.preventDefault();
+}
+
+function onPaste(event) {
+ let startPos = gDialog.srcInput.selectionStart;
+ if (startPos == undefined) {
+ return;
+ }
+ let endPos = gDialog.srcInput.selectionEnd;
+ let clipboard = event.clipboardData.getData("text/plain");
+
+ // We do out own paste by replacing the selection with the pre-processed
+ // clipboard data.
+ gDialog.srcInput.value =
+ gDialog.srcInput.value.substr(0, startPos) +
+ replaceDataURIs(clipboard) +
+ gDialog.srcInput.value.substr(endPos);
+ event.preventDefault();
+}
+
+function onAccept(event) {
+ let html = gDialog.srcInput.value;
+ if (!html) {
+ event.preventDefault();
+ return;
+ }
+
+ // Add back the original data URIs we stashed away earlier.
+ html = html.replace(
+ /(data:.+;base64,)([^"' >]+)/gi,
+ function (match, nonDataPart, key) {
+ if (!gFullDataStrings.has(key)) {
+ // User changed data URI.
+ return match;
+ }
+ return nonDataPart + gFullDataStrings.get(key);
+ }
+ );
+
+ try {
+ GetCurrentEditor().insertHTML(html);
+ } catch (e) {}
+ SaveWindowLocation();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml b/comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml
new file mode 100644
index 0000000000..1f35de996d
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertSource.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ style="min-height: 430px; min-width: 600px"
+ scrolling="false"
+>
+ <head>
+ <title>&windowTitle.label;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/messengercompose/editorUtilities.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/messengercompose/EdDialogCommon.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/messengercompose/EdInsSrc.js"
+ ></script>
+ </head>
+ <body>
+ <xul:dialog
+ buttonlabelaccept="&insertButton.label;"
+ buttonaccesskeyaccept="&insertButton.accesskey;"
+ >
+ <p id="srcMessage">&sourceEditField.label;</p>
+ <textarea id="srcInput" style="flex: 1" rows="18" cols="70"></textarea>
+ <p>
+ &example.label;
+ <code class="bold">
+ &exampleOpenTag.label;
+ <i>&exampleText.label;</i> &exampleCloseTag.label;
+ </code>
+ </p>
+ <hr />
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertChars.js b/comm/mail/components/compose/content/dialogs/EdInsertChars.js
new file mode 100644
index 0000000000..b710fb91a0
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertChars.js
@@ -0,0 +1,412 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+// ------------------------------------------------------------------
+// From Unicode 3.0 Page 54. 3.11 Conjoining Jamo Behavior
+var SBase = 0xac00;
+var LBase = 0x1100;
+var VBase = 0x1161;
+var TBase = 0x11a7;
+var LCount = 19;
+var VCount = 21;
+var TCount = 28;
+var NCount = VCount * TCount;
+// End of Unicode 3.0
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onClose);
+
+// dialog initialization code
+function Startup() {
+ if (!GetCurrentEditor()) {
+ window.close();
+ return;
+ }
+
+ StartupLatin();
+
+ // Set a variable on the opener window so we
+ // can track ownership of close this window with it
+ window.opener.InsertCharWindow = window;
+ window.sizeToContent();
+
+ SetWindowLocation();
+}
+
+function onAccept(event) {
+ // Insert the character
+ try {
+ GetCurrentEditor().insertText(LatinM.label);
+ } catch (e) {}
+
+ // Set persistent attributes to save
+ // which category, letter, and character modifier was used
+ CategoryGroup.setAttribute("category", category);
+ CategoryGroup.setAttribute("letter_index", indexL);
+ CategoryGroup.setAttribute("char_index", indexM);
+
+ // Don't close the dialog
+ event.preventDefault();
+}
+
+// Don't allow inserting in HTML Source Mode
+function onFocus() {
+ var enable = true;
+ if ("gEditorDisplayMode" in window.opener) {
+ enable = !window.opener.IsInHTMLSourceMode();
+ }
+
+ SetElementEnabled(
+ document.querySelector("dialog").getButton("accept"),
+ enable
+ );
+}
+
+function onClose() {
+ window.opener.InsertCharWindow = null;
+ SaveWindowLocation();
+}
+
+// ------------------------------------------------------------------
+var LatinL;
+var LatinM;
+var LatinL_Label;
+var LatinM_Label;
+var indexL = 0;
+var indexM = 0;
+var indexM_AU = 0;
+var indexM_AL = 0;
+var indexM_U = 0;
+var indexM_L = 0;
+var indexM_S = 0;
+var LItems = 0;
+var category;
+var CategoryGroup;
+var initialize = true;
+
+function StartupLatin() {
+ LatinL = document.getElementById("LatinL");
+ LatinM = document.getElementById("LatinM");
+ LatinL_Label = document.getElementById("LatinL_Label");
+ LatinM_Label = document.getElementById("LatinM_Label");
+
+ var Symbol = document.getElementById("Symbol");
+ var AccentUpper = document.getElementById("AccentUpper");
+ var AccentLower = document.getElementById("AccentLower");
+ var Upper = document.getElementById("Upper");
+ var Lower = document.getElementById("Lower");
+ CategoryGroup = document.getElementById("CatGrp");
+
+ // Initialize which radio button is set from persistent attribute...
+ var category = CategoryGroup.getAttribute("category");
+
+ // ...as well as indexes into the letter and character lists
+ var index = Number(CategoryGroup.getAttribute("letter_index"));
+ if (index && index >= 0) {
+ indexL = index;
+ }
+ index = Number(CategoryGroup.getAttribute("char_index"));
+ if (index && index >= 0) {
+ indexM = index;
+ }
+
+ switch (category) {
+ case "AccentUpper": // Uppercase Diacritical
+ CategoryGroup.selectedItem = AccentUpper;
+ indexM_AU = indexM;
+ break;
+ case "AccentLower": // Lowercase Diacritical
+ CategoryGroup.selectedItem = AccentLower;
+ indexM_AL = indexM;
+ break;
+ case "Upper": // Uppercase w/o Diacritical
+ CategoryGroup.selectedItem = Upper;
+ indexM_U = indexM;
+ break;
+ case "Lower": // Lowercase w/o Diacritical
+ CategoryGroup.selectedItem = Lower;
+ indexM_L = indexM;
+ break;
+ default:
+ category = "Symbol";
+ CategoryGroup.selectedItem = Symbol;
+ indexM_S = indexM;
+ break;
+ }
+
+ ChangeCategory(category);
+ initialize = false;
+}
+
+function ChangeCategory(newCategory) {
+ if (category != newCategory || initialize) {
+ category = newCategory;
+ // Note: Must do L before M to set LatinL.selectedIndex
+ UpdateLatinL();
+ UpdateLatinM();
+ UpdateCharacter();
+ }
+}
+
+function SelectLatinLetter() {
+ if (LatinL.selectedIndex != indexL) {
+ indexL = LatinL.selectedIndex;
+ UpdateLatinM();
+ UpdateCharacter();
+ }
+}
+
+function SelectLatinModifier() {
+ if (LatinM.selectedIndex != indexM) {
+ indexM = LatinM.selectedIndex;
+ UpdateCharacter();
+ }
+}
+function DisableLatinL(disable) {
+ if (disable) {
+ LatinL_Label.setAttribute("disabled", "true");
+ LatinL.setAttribute("disabled", "true");
+ } else {
+ LatinL_Label.removeAttribute("disabled");
+ LatinL.removeAttribute("disabled");
+ }
+}
+
+function UpdateLatinL() {
+ LatinL.removeAllItems();
+ if (category == "AccentUpper" || category == "AccentLower") {
+ DisableLatinL(false);
+ // No Q or q
+ var alphabet =
+ category == "AccentUpper"
+ ? "ABCDEFGHIJKLMNOPRSTUVWXYZ"
+ : "abcdefghijklmnoprstuvwxyz";
+ for (var letter = 0; letter < alphabet.length; letter++) {
+ LatinL.appendItem(alphabet.charAt(letter));
+ }
+
+ LatinL.selectedIndex = indexL;
+ } else {
+ // Other categories don't hinge on a "letter"
+ DisableLatinL(true);
+ // Note: don't change the indexL so it can be used next time
+ }
+}
+
+function UpdateLatinM() {
+ LatinM.removeAllItems();
+ var i, accent;
+ switch (category) {
+ case "AccentUpper": // Uppercase Diacritical
+ accent = upper[indexL];
+ for (i = 0; i < accent.length; i++) {
+ LatinM.appendItem(accent.charAt(i));
+ }
+
+ if (indexM_AU < accent.length) {
+ indexM = indexM_AU;
+ } else {
+ indexM = accent.length - 1;
+ }
+ indexM_AU = indexM;
+ break;
+
+ case "AccentLower": // Lowercase Diacritical
+ accent = lower[indexL];
+ for (i = 0; i < accent.length; i++) {
+ LatinM.appendItem(accent.charAt(i));
+ }
+
+ if (indexM_AL < accent.length) {
+ indexM = indexM_AL;
+ } else {
+ indexM = lower[indexL].length - 1;
+ }
+ indexM_AL = indexM;
+ break;
+
+ case "Upper": // Uppercase w/o Diacritical
+ for (i = 0; i < otherupper.length; i++) {
+ LatinM.appendItem(otherupper.charAt(i));
+ }
+
+ if (indexM_U < otherupper.length) {
+ indexM = indexM_U;
+ } else {
+ indexM = otherupper.length - 1;
+ }
+ indexM_U = indexM;
+ break;
+
+ case "Lower": // Lowercase w/o Diacritical
+ for (i = 0; i < otherlower.length; i++) {
+ LatinM.appendItem(otherlower.charAt(i));
+ }
+
+ if (indexM_L < otherlower.length) {
+ indexM = indexM_L;
+ } else {
+ indexM = otherlower.length - 1;
+ }
+ indexM_L = indexM;
+ break;
+
+ case "Symbol": // Symbol
+ for (i = 0; i < symbol.length; i++) {
+ LatinM.appendItem(symbol.charAt(i));
+ }
+
+ if (indexM_S < symbol.length) {
+ indexM = indexM_S;
+ } else {
+ indexM = symbol.length - 1;
+ }
+ indexM_S = indexM;
+ break;
+ }
+ LatinM.selectedIndex = indexM;
+}
+
+function UpdateCharacter() {
+ indexM = LatinM.selectedIndex;
+
+ switch (category) {
+ case "AccentUpper": // Uppercase Diacritical
+ indexM_AU = indexM;
+ break;
+ case "AccentLower": // Lowercase Diacritical
+ indexM_AL = indexM;
+ break;
+ case "Upper": // Uppercase w/o Diacritical
+ indexM_U = indexM;
+ break;
+ case "Lower": // Lowercase w/o Diacritical
+ indexM_L = indexM;
+ break;
+ case "Symbol":
+ indexM_S = indexM;
+ break;
+ }
+ // dump("Letter Index="+indexL+", Character Index="+indexM+", Character = "+LatinM.label+"\n");
+}
+
+const upper = [
+ // A
+ "\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u0100\u0102\u0104\u01cd\u01de\u01de\u01e0\u01fa\u0200\u0202\u0226\u1e00\u1ea0\u1ea2\u1ea4\u1ea6\u1ea8\u1eaa\u1eac\u1eae\u1eb0\u1eb2\u1eb4\u1eb6",
+ // B
+ "\u0181\u0182\u0184\u1e02\u1e04\u1e06",
+ // C
+ "\u00c7\u0106\u0108\u010a\u010c\u0187\u1e08",
+ // D
+ "\u010e\u0110\u0189\u018a\u1e0a\u1e0c\u1e0e\u1e10\u1e12",
+ // E
+ "\u00C8\u00C9\u00CA\u00CB\u0112\u0114\u0116\u0118\u011A\u0204\u0206\u0228\u1e14\u1e16\u1e18\u1e1a\u1e1c\u1eb8\u1eba\u1ebc\u1ebe\u1ec0\u1ec2\u1ec4\u1ec6",
+ // F
+ "\u1e1e",
+ // G
+ "\u011c\u011E\u0120\u0122\u01e4\u01e6\u01f4\u1e20",
+ // H
+ "\u0124\u0126\u021e\u1e22\u1e24\u1e26\u1e28\u1e2a",
+ // I
+ "\u00CC\u00CD\u00CE\u00CF\u0128\u012a\u012C\u012e\u0130\u0208\u020a\u1e2c\u1e2e\u1ec8\u1eca",
+ // J
+ "\u0134\u01f0",
+ // K
+ "\u0136\u0198\u01e8\u1e30\u1e32\u1e34",
+ // L
+ "\u0139\u013B\u013D\u013F\u0141\u1e36\u1e38\u1e3a\u1e3c",
+ // M
+ "\u1e3e\u1e40\u1e42",
+ // N
+ "\u00D1\u0143\u0145\u0147\u014A\u01F8\u1e44\u1e46\u1e48\u1e4a",
+ // O
+ "\u00D2\u00D3\u00D4\u00D5\u00D6\u014C\u014E\u0150\u01ea\u01ec\u020c\u020e\u022A\u022C\u022E\u0230\u1e4c\u1e4e\u1e50\u1e52\u1ecc\u1ece\u1ed0\u1ed2\u1ed4\u1ed6\u1ed8\u1eda\u1edc\u1ede\u1ee0\u1ee2",
+ // P
+ "\u1e54\u1e56",
+ // No Q
+ // R
+ "\u0154\u0156\u0158\u0210\u0212\u1e58\u1e5a\u1e5c\u1e5e",
+ // S
+ "\u015A\u015C\u015E\u0160\u0218\u1e60\u1e62\u1e64\u1e66\u1e68",
+ // T
+ "\u0162\u0164\u0166\u021A\u1e6a\u1e6c\u1e6e\u1e70",
+ // U
+ "\u00D9\u00DA\u00DB\u00DC\u0168\u016A\u016C\u016E\u0170\u0172\u0214\u0216\u1e72\u1e74\u1e76\u1e78\u1e7a\u1ee4\u1ee6\u1ee8\u1eea\u1eec\u1eee\u1ef0",
+ // V
+ "\u1e7c\u1e7e",
+ // W
+ "\u0174\u1e80\u1e82\u1e84\u1e86\u1e88",
+ // X
+ "\u1e8a\u1e8c",
+ // Y
+ "\u00DD\u0176\u0178\u0232\u1e8e\u1ef2\u1ef4\u1ef6\u1ef8",
+ // Z
+ "\u0179\u017B\u017D\u0224\u1e90\u1e92\u1e94",
+];
+
+const lower = [
+ // a
+ "\u00e0\u00e1\u00e2\u00e3\u00e4\u00e5\u0101\u0103\u0105\u01ce\u01df\u01e1\u01fb\u0201\u0203\u0227\u1e01\u1e9a\u1ea1\u1ea3\u1ea5\u1ea7\u1ea9\u1eab\u1ead\u1eaf\u1eb1\u1eb3\u1eb5\u1eb7",
+ // b
+ "\u0180\u0183\u0185\u1e03\u1e05\u1e07",
+ // c
+ "\u00e7\u0107\u0109\u010b\u010d\u0188\u1e09",
+ // d
+ "\u010f\u0111\u1e0b\u1e0d\u1e0f\u1e11\u1e13",
+ // e
+ "\u00e8\u00e9\u00ea\u00eb\u0113\u0115\u0117\u0119\u011b\u0205\u0207\u0229\u1e15\u1e17\u1e19\u1e1b\u1e1d\u1eb9\u1ebb\u1ebd\u1ebf\u1ec1\u1ec3\u1ec5\u1ec7",
+ // f
+ "\u1e1f",
+ // g
+ "\u011d\u011f\u0121\u0123\u01e5\u01e7\u01f5\u1e21",
+ // h
+ "\u0125\u0127\u021f\u1e23\u1e25\u1e27\u1e29\u1e2b\u1e96",
+ // i
+ "\u00ec\u00ed\u00ee\u00ef\u0129\u012b\u012d\u012f\u0131\u01d0\u0209\u020b\u1e2d\u1e2f\u1ec9\u1ecb",
+ // j
+ "\u0135",
+ // k
+ "\u0137\u0138\u01e9\u1e31\u1e33\u1e35",
+ // l
+ "\u013a\u013c\u013e\u0140\u0142\u1e37\u1e39\u1e3b\u1e3d",
+ // m
+ "\u1e3f\u1e41\u1e43",
+ // n
+ "\u00f1\u0144\u0146\u0148\u0149\u014b\u01f9\u1e45\u1e47\u1e49\u1e4b",
+ // o
+ "\u00f2\u00f3\u00f4\u00f5\u00f6\u014d\u014f\u0151\u01d2\u01eb\u01ed\u020d\u020e\u022b\u022d\u022f\u0231\u1e4d\u1e4f\u1e51\u1e53\u1ecd\u1ecf\u1ed1\u1ed3\u1ed5\u1ed7\u1ed9\u1edb\u1edd\u1edf\u1ee1\u1ee3",
+ // p
+ "\u1e55\u1e57",
+ // No q
+ // r
+ "\u0155\u0157\u0159\u0211\u0213\u1e59\u1e5b\u1e5d\u1e5f",
+ // s
+ "\u015b\u015d\u015f\u0161\u0219\u1e61\u1e63\u1e65\u1e67\u1e69",
+ // t
+ "\u0162\u0163\u0165\u0167\u021b\u1e6b\u1e6d\u1e6f\u1e71\u1e97",
+ // u
+ "\u00f9\u00fa\u00fb\u00fc\u0169\u016b\u016d\u016f\u0171\u0173\u01d4\u01d6\u01d8\u01da\u01dc\u0215\u0217\u1e73\u1e75\u1e77\u1e79\u1e7b\u1ee5\u1ee7\u1ee9\u1eeb\u1eed\u1eef\u1ef1",
+ // v
+ "\u1e7d\u1e7f",
+ // w
+ "\u0175\u1e81\u1e83\u1e85\u1e87\u1e89\u1e98",
+ // x
+ "\u1e8b\u1e8d",
+ // y
+ "\u00fd\u00ff\u0177\u0233\u1e8f\u1e99\u1ef3\u1ef5\u1ef7\u1ef9",
+ // z
+ "\u017a\u017c\u017e\u0225\u1e91\u1e93\u1e95",
+];
+
+const symbol =
+ "\u00a1\u00a2\u00a3\u00a4\u00a5\u20ac\u00a6\u00a7\u00a8\u00a9\u00aa\u00ab\u00ac\u00ae\u00af\u00b0\u00b1\u00b2\u00b3\u00b4\u00b5\u00b6\u00b7\u00b8\u00b9\u00ba\u00bb\u00bc\u00bd\u00be\u00bf\u00d7\u00f7";
+
+const otherupper =
+ "\u00c6\u00d0\u00d8\u00de\u0132\u0152\u0186\u01c4\u01c5\u01c7\u01c8\u01ca\u01cb\u01F1\u01f2";
+
+const otherlower =
+ "\u00e6\u00f0\u00f8\u00fe\u00df\u0133\u0153\u01c6\u01c9\u01cc\u01f3";
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml
new file mode 100644
index 0000000000..c610abdd88
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml
@@ -0,0 +1,92 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/EdInsertChars.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertChars.dtd">
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="Startup()"
+ onfocus="onFocus()"
+ lightweightthemes="true"
+ style="min-width: 20em"
+>
+ <dialog
+ id="insertCharsDlg"
+ buttonlabelaccept="&insertButton.label;"
+ buttonlabelcancel="&closeButton.label;"
+ >
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdInsertChars.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <html:fieldset>
+ <html:legend>&category.label;</html:legend>
+ <radiogroup id="CatGrp" persist="category letter_index char_index">
+ <radio
+ id="AccentUpper"
+ label="&accentUpper.label;"
+ oncommand="ChangeCategory(this.id)"
+ />
+ <radio
+ id="AccentLower"
+ label="&accentLower.label;"
+ oncommand="ChangeCategory(this.id)"
+ />
+ <radio
+ id="Upper"
+ label="&otherUpper.label;"
+ oncommand="ChangeCategory(this.id)"
+ />
+ <radio
+ id="Lower"
+ label="&otherLower.label;"
+ oncommand="ChangeCategory(this.id)"
+ />
+ <radio
+ id="Symbol"
+ label="&commonSymbols.label;"
+ oncommand="ChangeCategory(this.id)"
+ />
+ </radiogroup>
+ <spacer class="spacer" />
+ </html:fieldset>
+ <html:div class="grid-two-column-equalsize">
+ <!-- value is set in JS from editor.properties strings -->
+ <label
+ id="LatinL_Label"
+ control="LatinL"
+ value="&letter.label;"
+ accesskey="&letter.accessKey;"
+ />
+ <menulist id="LatinL" oncommand="SelectLatinLetter()">
+ <menupopup />
+ </menulist>
+ <label
+ id="LatinM_Label"
+ control="LatinM"
+ value="&character.label;"
+ accesskey="&character.accessKey;"
+ />
+ <menulist id="LatinM" oncommand="SelectLatinModifier()">
+ <menupopup />
+ </menulist>
+ </html:div>
+ <separator class="groove" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertMath.js b/comm/mail/components/compose/content/dialogs/EdInsertMath.js
new file mode 100644
index 0000000000..a60a3affcc
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertMath.js
@@ -0,0 +1,317 @@
+/* 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/. */
+
+/* Insert MathML dialog */
+
+/* import-globals-from ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+
+ // Create dialog object for easy access
+ gDialog.accept = document.querySelector("dialog").getButton("accept");
+ gDialog.mode = document.getElementById("optionMode");
+ gDialog.direction = document.getElementById("optionDirection");
+ gDialog.input = document.getElementById("input");
+ gDialog.output = document.getElementById("output");
+ gDialog.tabbox = document.getElementById("tabboxInsertLaTeXCommand");
+
+ // Set initial focus
+ gDialog.input.focus();
+
+ // Load TeXZilla
+ // TeXZilla.js contains non-ASCII characters and explicitly sets
+ // window.TeXZilla, so we have to specify the charset parameter but don't
+ // need to worry about the targetObj parameter.
+ /* globals TeXZilla */
+ Services.scriptloader.loadSubScript(
+ "chrome://messenger/content/messengercompose/TeXZilla.js",
+ {},
+ "UTF-8"
+ );
+
+ // Verify if the selection is on a <math> and initialize the dialog.
+ gDialog.oldMath = editor.getElementOrParentByTagName("math", null);
+ if (gDialog.oldMath) {
+ // When these attributes are absent or invalid, they default to "inline" and "ltr" respectively.
+ gDialog.mode.selectedIndex =
+ gDialog.oldMath.getAttribute("display") == "block" ? 1 : 0;
+ gDialog.direction.selectedIndex =
+ gDialog.oldMath.getAttribute("dir") == "rtl" ? 1 : 0;
+ gDialog.input.value = TeXZilla.getTeXSource(gDialog.oldMath);
+ }
+
+ // Create the tabbox with LaTeX commands.
+ createCommandPanel({
+ "√⅗²": [
+ "{⋯}^{⋯}",
+ "{⋯}_{⋯}",
+ "{⋯}_{⋯}^{⋯}",
+ "\\underset{⋯}{⋯}",
+ "\\overset{⋯}{⋯}",
+ "\\underoverset{⋯}{⋯}{⋯}",
+ "\\left(⋯\\right)",
+ "\\left[⋯\\right]",
+ "\\frac{⋯}{⋯}",
+ "\\binom{⋯}{⋯}",
+ "\\sqrt{⋯}",
+ "\\sqrt[⋯]{⋯}",
+ "\\cos\\left({⋯}\\right)",
+ "\\sin\\left({⋯}\\right)",
+ "\\tan\\left({⋯}\\right)",
+ "\\exp\\left({⋯}\\right)",
+ "\\ln\\left({⋯}\\right)",
+ "\\underbrace{⋯}",
+ "\\underline{⋯}",
+ "\\overbrace{⋯}",
+ "\\widevec{⋯}",
+ "\\widetilde{⋯}",
+ "\\widehat{⋯}",
+ "\\widecheck{⋯}",
+ "\\widebar{⋯}",
+ "\\dot{⋯}",
+ "\\ddot{⋯}",
+ "\\boxed{⋯}",
+ "\\slash{⋯}",
+ ],
+ "(▦)": [
+ "\\begin{matrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{matrix}",
+ "\\begin{pmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{pmatrix}",
+ "\\begin{bmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{bmatrix}",
+ "\\begin{Bmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{Bmatrix}",
+ "\\begin{vmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{vmatrix}",
+ "\\begin{Vmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{Vmatrix}",
+ "\\begin{cases} ⋯ \\\\ ⋯ \\end{cases}",
+ "\\begin{aligned} ⋯ &= ⋯ \\\\ ⋯ &= ⋯ \\end{aligned}",
+ ],
+ });
+ createSymbolPanels([
+ "∏∐∑∫∬∭⨌∮⊎⊕⊖⊗⊘⊙⋀⋁⋂⋃⌈⌉⌊⌋⎰⎱⟨⟩⟪⟫∥⫼⨀⨁⨂⨄⨅⨆ðıȷℏℑℓ℘ℜℵℶ",
+ "∀∃∄∅∉∊∋∌⊂⊃⊄⊅⊆⊇⊈⊈⊉⊊⊊⊋⊋⊏⊐⊑⊒⊓⊔⊥⋐⋑⋔⫅⫆⫋⫋⫌⫌…⋮⋯⋰⋱♭♮♯∂∇",
+ "±×÷†‡•∓∔∗∘∝∠∡∢∧∨∴∵∼∽≁≃≅≇≈≈≊≍≎≏≐≑≒≓≖≗≜≡≢≬⊚⊛⊞⊡⊢⊣⊤⊥",
+ "⊨⊩⊪⊫⊬⊭⊯⊲⊲⊳⊴⊵⊸⊻⋄⋅⋇⋈⋉⋊⋋⋌⋍⋎⋏⋒⋓⌅⌆⌣△▴▵▸▹▽▾▿◂◃◊○★♠♡♢♣⧫",
+ "≦≧≨≩≩≪≫≮≯≰≱≲≳≶≷≺≻≼≽≾≿⊀⊁⋖⋗⋘⋙⋚⋛⋞⋟⋦⋧⋨⋩⩽⩾⪅⪆⪇⪈⪉⪊⪋⪌⪕⪯⪰⪷⪸⪹⪺",
+ "←↑→↓↔↕↖↗↘↙↜↝↞↠↢↣↦↩↪↫↬↭↭↰↱↼↽↾↿⇀⇁⇂⇃⇄⇆⇇⇈⇉⇊⇋⇌⇐⇑⇒⇓⇕⇖⇗⇘⇙⟺",
+ "αβγδϵ϶εζηθϑικϰλμνξℴπϖρϱσςτυϕφχψωΓΔΘΛΞΠΣϒΦΨΩϝ℧",
+ "𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ",
+ "𝒶𝒷𝒸𝒹ℯ𝒻ℊ𝒽𝒾𝒿𝓀𝓁𝓂𝓃ℴ𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏𝒜ℬ𝒞𝒟ℰℱ𝒢ℋℐ𝒥𝒦ℒℳ𝒩𝒪𝒫𝒬ℛ𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵",
+ "𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ",
+ ]);
+ gDialog.tabbox.selectedIndex = 0;
+
+ updateMath();
+
+ SetWindowLocation();
+}
+
+function insertLaTeXCommand(aButton) {
+ gDialog.input.focus();
+
+ // For a single math symbol, just use the insertText command.
+ if (aButton.label) {
+ gDialog.input.editor.insertText(aButton.label);
+ return;
+ }
+
+ // Otherwise, it's a LaTeX command with at least one argument...
+ var latex = TeXZilla.getTeXSource(aButton.firstElementChild);
+ var selectionStart = gDialog.input.selectionStart;
+ var selectionEnd = gDialog.input.selectionEnd;
+
+ // If the selection is not empty, we replace the first argument of the LaTeX
+ // command with the current selection.
+ var selection = gDialog.input.value.substring(selectionStart, selectionEnd);
+ if (selection != "") {
+ latex = latex.replace("⋯", selection);
+ }
+
+ // Try and move to the next position.
+ var latexNewStart = latex.indexOf("⋯"),
+ latexNewEnd;
+ if (latexNewStart == -1) {
+ // This is a unary function and the selection was used as an argument above.
+ // We select the expression again so that one can choose to apply further
+ // command to it or just move the caret after that text.
+ latexNewStart = 0;
+ latexNewEnd = latex.length;
+ } else {
+ // Otherwise, select the dots representing the next argument.
+ latexNewEnd = latexNewStart + 1;
+ }
+
+ // Update the input text and selection.
+ gDialog.input.editor.insertText(latex);
+ gDialog.input.setSelectionRange(
+ selectionStart + latexNewStart,
+ selectionStart + latexNewEnd
+ );
+
+ updateMath();
+}
+
+function createCommandPanel(aCommandPanelList) {
+ const columnCount = 10;
+
+ for (var label in aCommandPanelList) {
+ var commands = aCommandPanelList[label];
+
+ // Create the <table> element with the <tr>.
+ var table = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "table"
+ );
+
+ var i = 0,
+ row;
+ for (var command of commands) {
+ if (i % columnCount == 0) {
+ // Create a new row.
+ row = document.createElementNS("http://www.w3.org/1999/xhtml", "tr");
+ table.appendChild(row);
+ }
+
+ // Create a new button to insert the symbol.
+ var button = document.createXULElement("toolbarbutton");
+ var td = document.createElementNS("http://www.w3.org/1999/xhtml", "td");
+ button.setAttribute("class", "tabbable");
+ button.appendChild(TeXZilla.toMathML(command));
+ td.append(button);
+ row.appendChild(td);
+
+ i++;
+ }
+
+ // Create a new <tab> element.
+ var tab = document.createXULElement("tab");
+ tab.setAttribute("label", label);
+ gDialog.tabbox.tabs.appendChild(tab);
+
+ // Append the new tab panel.
+ gDialog.tabbox.tabpanels.appendChild(table);
+ }
+}
+
+function createSymbolPanels(aSymbolPanelList) {
+ const columnCount = 13,
+ tabLabelLength = 3;
+
+ for (var symbols of aSymbolPanelList) {
+ // Create the <table> element with the <tr>.
+ var table = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "table"
+ );
+ var i = 0,
+ tabLabel = "",
+ row;
+ for (var symbol of symbols) {
+ if (i % columnCount == 0) {
+ // Create a new row.
+ row = document.createElementNS("http://www.w3.org/1999/xhtml", "tr");
+ table.appendChild(row);
+ }
+
+ // Build the tab label from the first symbols of this tab.
+ if (i < tabLabelLength) {
+ tabLabel += symbol;
+ }
+
+ // Create a new button to insert the symbol.
+ var button = document.createXULElement("toolbarbutton");
+ var td = document.createElementNS("http://www.w3.org/1999/xhtml", "td");
+ button.setAttribute("label", symbol);
+ button.setAttribute("class", "tabbable");
+ td.append(button);
+ row.appendChild(td);
+
+ i++;
+ }
+
+ // Create a new <tab> element with the label determined above.
+ var tab = document.createXULElement("tab");
+ tab.setAttribute("label", tabLabel);
+ gDialog.tabbox.tabs.appendChild(tab);
+
+ // Append the new tab panel.
+ gDialog.tabbox.tabpanels.appendChild(table);
+ }
+}
+
+function onAccept(event) {
+ if (gDialog.output.firstElementChild) {
+ var editor = GetCurrentEditor();
+ editor.beginTransaction();
+
+ try {
+ var newMath = editor.document.importNode(
+ gDialog.output.firstElementChild,
+ true
+ );
+ if (gDialog.oldMath) {
+ // Replace the old <math> element with the new one.
+ editor.selectElement(gDialog.oldMath);
+ editor.insertElementAtSelection(newMath, true);
+ } else {
+ // Insert the new <math> element.
+ editor.insertElementAtSelection(newMath, false);
+ }
+ } catch (e) {}
+
+ editor.endTransaction();
+ } else {
+ dump("Null value -- not inserting in MathML Source dialog\n");
+ event.preventDefault();
+ }
+ SaveWindowLocation();
+}
+
+function updateMath() {
+ // Remove the preview, if any.
+ if (gDialog.output.firstElementChild) {
+ gDialog.output.firstElementChild.remove();
+ }
+
+ // Try to convert the LaTeX source into MathML using TeXZilla.
+ // We use the placeholder text if no input is provided.
+ try {
+ var input = gDialog.input.value || gDialog.input.placeholder;
+ var newMath = TeXZilla.toMathML(
+ input,
+ gDialog.mode.selectedIndex,
+ gDialog.direction.selectedIndex,
+ true
+ );
+ gDialog.output.appendChild(document.importNode(newMath, true));
+ gDialog.output.style.opacity = gDialog.input.value ? 1 : 0.5;
+ } catch (e) {}
+ // Disable the accept button if parsing fails or when the placeholder is used.
+ gDialog.accept.disabled =
+ !gDialog.input.value || !gDialog.output.firstElementChild;
+}
+
+function updateMode() {
+ if (gDialog.output.firstElementChild) {
+ gDialog.output.firstElementChild.setAttribute(
+ "display",
+ gDialog.mode.selectedIndex ? "block" : "inline"
+ );
+ }
+}
+
+function updateDirection() {
+ if (gDialog.output.firstElementChild) {
+ gDialog.output.firstElementChild.setAttribute(
+ "dir",
+ gDialog.direction.selectedIndex ? "rtl" : "ltr"
+ );
+ }
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml
new file mode 100644
index 0000000000..d76a518b0a
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertMath.dtd">
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup();"
+>
+ <dialog
+ buttonlabelaccept="&insertButton.label;"
+ buttonaccesskeyaccept="&insertButton.accesskey;"
+ >
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdInsertMath.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <label id="srcMessage" value="&sourceEditField.label;" />
+ <html:textarea
+ id="input"
+ rows="5"
+ oninput="updateMath();"
+ placeholder="\sqrt{x_1} + \frac{π^3}{2}"
+ />
+ <vbox flex="1" style="overflow: auto; width: 30em; height: 5em">
+ <description id="output" />
+ </vbox>
+ <tabbox id="tabboxInsertLaTeXCommand">
+ <tabs />
+ <tabpanels oncommand="insertLaTeXCommand(event.target);" />
+ </tabbox>
+ <spacer class="spacer" />
+ <html:fieldset>
+ <html:legend>&options.label;</html:legend>
+ <hbox>
+ <radiogroup id="optionMode" oncommand="updateMode();">
+ <radio
+ label="&optionInline.label;"
+ accesskey="&optionInline.accesskey;"
+ />
+ <radio
+ label="&optionDisplay.label;"
+ accesskey="&optionDisplay.accesskey;"
+ />
+ </radiogroup>
+ <radiogroup id="optionDirection" oncommand="updateDirection();">
+ <radio label="&optionLTR.label;" accesskey="&optionLTR.accesskey;" />
+ <radio label="&optionRTL.label;" accesskey="&optionRTL.accesskey;" />
+ </radiogroup>
+ </hbox>
+ </html:fieldset>
+ <spacer class="spacer" />
+ <separator class="groove" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTOC.js b/comm/mail/components/compose/content/dialogs/EdInsertTOC.js
new file mode 100644
index 0000000000..45d0972f3b
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertTOC.js
@@ -0,0 +1,378 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+// tocHeadersArray is the array containing the pairs tag/class
+// defining TOC entries
+var tocHeadersArray = new Array(6);
+
+// a global used when building the TOC
+var currentHeaderLevel = 0;
+
+// a global set to true if the TOC is to be readonly
+var readonly = false;
+
+// a global set to true if user wants indexes in the TOC
+var orderedList = true;
+
+// constants
+const kMozToc = "mozToc";
+const kMozTocLength = 6;
+const kMozTocIdPrefix = "mozTocId";
+const kMozTocIdPrefixLength = 8;
+const kMozTocClassPrefix = "mozToc";
+const kMozTocClassPrefixLength = 6;
+
+document.addEventListener("dialogaccept", () => BuildTOC(true));
+
+// Startup() is called when EdInsertTOC.xhtml is opened
+function Startup() {
+ // early way out if if we have no editor
+ if (!GetCurrentEditor()) {
+ window.close();
+ return;
+ }
+
+ var i;
+ // clean the table of tag/class pairs we look for
+ for (i = 0; i < 6; ++i) {
+ tocHeadersArray[i] = ["", ""];
+ }
+
+ // reset all settings
+ for (i = 1; i < 7; ++i) {
+ var menulist = document.getElementById("header" + i + "Menulist");
+ var menuitem = document.getElementById("header" + i + "none");
+ var textbox = document.getElementById("header" + i + "Class");
+ menulist.selectedItem = menuitem;
+ textbox.setAttribute("disabled", "true");
+ }
+
+ var theDocument = GetCurrentEditor().document;
+
+ // do we already have a TOC in the document ? It should have "mozToc" ID
+ var toc = theDocument.getElementById(kMozToc);
+
+ // default TOC definition, use h1-h6 for TOC entry levels 1-6
+ var headers = "h1 1 h2 2 h3 3 h4 4 h5 5 h6 6";
+
+ var orderedListCheckbox = document.getElementById("orderedListCheckbox");
+ orderedListCheckbox.checked = true;
+
+ if (toc) {
+ // man, there is already a TOC here
+
+ if (toc.getAttribute("class") == "readonly") {
+ // and it's readonly
+ var checkbox = document.getElementById("readOnlyCheckbox");
+ checkbox.checked = true;
+ readonly = true;
+ }
+
+ // let's see if it's an OL or an UL
+ orderedList = toc.nodeName.toLowerCase() == "ol";
+ orderedListCheckbox.checked = orderedList;
+
+ var nodeList = toc.childNodes;
+ // let's look at the children of the TOC ; if we find a comment beginning
+ // with "mozToc", it contains the TOC definition
+ for (i = 0; i < nodeList.length; ++i) {
+ if (
+ nodeList.item(i).nodeType == Node.COMMENT_NODE &&
+ nodeList.item(i).data.startsWith(kMozToc)
+ ) {
+ // yep, there is already a definition here; parse it !
+ headers = nodeList
+ .item(i)
+ .data.substr(
+ kMozTocLength + 1,
+ nodeList.item(i).length - kMozTocLength - 1
+ );
+ break;
+ }
+ }
+ }
+
+ // let's get an array filled with the (tag.class, index level) pairs
+ var headersArray = headers.split(" ");
+
+ for (i = 0; i < headersArray.length; i += 2) {
+ var tag = headersArray[i],
+ className = "";
+ var index = headersArray[i + 1];
+ menulist = document.getElementById("header" + index + "Menulist");
+ if (menulist) {
+ var sep = tag.indexOf(".");
+ if (sep != -1) {
+ // the tag variable contains in fact "tag.className", let's parse
+ // the class and get the real tag name
+ var tmp = tag.substr(0, sep);
+ className = tag.substr(sep + 1, tag.length - sep - 1);
+ tag = tmp;
+ }
+
+ // update the dialog
+ menuitem = document.getElementById("header" + index + tag.toUpperCase());
+ textbox = document.getElementById("header" + index + "Class");
+ menulist.selectedItem = menuitem;
+ if (tag != "") {
+ textbox.removeAttribute("disabled");
+ }
+ if (className != "") {
+ textbox.value = className;
+ }
+ tocHeadersArray[index - 1] = [tag, className];
+ }
+ }
+}
+
+function BuildTOC(update) {
+ // controlClass() is a node filter that accepts a node if
+ // (a) we don't look for a class (b) we look for a class and
+ // node has it
+ function controlClass(node, index) {
+ currentHeaderLevel = index + 1;
+ if (tocHeadersArray[index][1] == "") {
+ // we are not looking for a specific class, this node is ok
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ if (node.getAttribute("class")) {
+ // yep, we look for a class, let's look at all the classes
+ // the node has
+ var classArray = node.getAttribute("class").split(" ");
+ for (var j = 0; j < classArray.length; j++) {
+ if (classArray[j] == tocHeadersArray[index][1]) {
+ // hehe, we found it...
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }
+ }
+ return NodeFilter.FILTER_SKIP;
+ }
+
+ // the main node filter for our node iterator
+ // it selects the tag names as specified in the dialog
+ // then calls the controlClass filter above
+ function acceptNode(node) {
+ switch (node.nodeName.toLowerCase()) {
+ case tocHeadersArray[0][0]:
+ return controlClass(node, 0);
+ case tocHeadersArray[1][0]:
+ return controlClass(node, 1);
+ case tocHeadersArray[2][0]:
+ return controlClass(node, 2);
+ case tocHeadersArray[3][0]:
+ return controlClass(node, 3);
+ case tocHeadersArray[4][0]:
+ return controlClass(node, 4);
+ case tocHeadersArray[5][0]:
+ return controlClass(node, 5);
+ default:
+ return NodeFilter.FILTER_SKIP;
+ }
+ }
+
+ var editor = GetCurrentEditor();
+ var theDocument = editor.document;
+ // let's create a TreeWalker to look for our nodes
+ var treeWalker = theDocument.createTreeWalker(
+ theDocument.documentElement,
+ NodeFilter.SHOW_ELEMENT,
+ acceptNode,
+ true
+ );
+ // we need an array to store all TOC entries we find in the document
+ var tocArray = [];
+ if (treeWalker) {
+ var tocSourceNode = treeWalker.nextNode();
+ while (tocSourceNode) {
+ var headerIndex = currentHeaderLevel;
+
+ // we have a node, we need to get all its textual contents
+ var textTreeWalker = theDocument.createTreeWalker(
+ tocSourceNode,
+ NodeFilter.SHOW_TEXT,
+ null,
+ true
+ );
+ var textNode = textTreeWalker.nextNode(),
+ headerText = "";
+ while (textNode) {
+ headerText += textNode.data;
+ textNode = textTreeWalker.nextNode();
+ }
+
+ var anchor = tocSourceNode.firstChild,
+ id;
+ // do we have a named anchor as 1st child of our node ?
+ if (
+ anchor.nodeName.toLowerCase() == "a" &&
+ anchor.hasAttribute("name") &&
+ anchor.getAttribute("name").startsWith(kMozTocIdPrefix)
+ ) {
+ // yep, get its name
+ id = anchor.getAttribute("name");
+ } else {
+ // no we don't and we need to create one
+ anchor = theDocument.createElement("a");
+ tocSourceNode.insertBefore(anchor, tocSourceNode.firstChild);
+ // let's give it a random ID
+ var c = 1000000 * Math.random();
+ id = kMozTocIdPrefix + Math.round(c);
+ anchor.setAttribute("name", id);
+ anchor.setAttribute(
+ "class",
+ kMozTocClassPrefix + tocSourceNode.nodeName.toUpperCase()
+ );
+ }
+ // and store that new entry in our array
+ tocArray.push(headerIndex, headerText, id);
+ tocSourceNode = treeWalker.nextNode();
+ }
+ }
+
+ /* generate the TOC itself */
+ headerIndex = 0;
+ var item, toc;
+ for (var i = 0; i < tocArray.length; i += 3) {
+ if (!headerIndex) {
+ // do we need to create an ol/ul container for the first entry ?
+ ++headerIndex;
+ toc = theDocument.getElementById(kMozToc);
+ if (!toc || !update) {
+ // we need to create a list container for the table of contents
+ toc = GetCurrentEditor().createElementWithDefaults(
+ orderedList ? "ol" : "ul"
+ );
+ // grrr, we need to create a LI inside the list otherwise
+ // Composer will refuse an empty list and will remove it !
+ var pit = theDocument.createElement("li");
+ toc.appendChild(pit);
+ GetCurrentEditor().insertElementAtSelection(toc, true);
+ // ah, now it's inserted so let's remove the useless list item...
+ toc.removeChild(pit);
+ // we need to recognize later that this list is our TOC
+ toc.setAttribute("id", kMozToc);
+ } else if (orderedList != (toc.nodeName.toLowerCase() == "ol")) {
+ // we have to update an existing TOC, is the existing TOC of the
+ // desired type (ordered or not) ?
+
+ // nope, we have to recreate the list
+ var newToc = GetCurrentEditor().createElementWithDefaults(
+ orderedList ? "ol" : "ul"
+ );
+ toc.parentNode.insertBefore(newToc, toc);
+ // and remove the old one
+ toc.remove();
+ toc = newToc;
+ toc.setAttribute("id", kMozToc);
+ } else {
+ // we can keep the list itself but let's get rid of the TOC entries
+ while (toc.hasChildNodes()) {
+ toc.lastChild.remove();
+ }
+ }
+
+ var commentText = "mozToc ";
+ for (var j = 0; j < 6; j++) {
+ if (tocHeadersArray[j][0] != "") {
+ commentText += tocHeadersArray[j][0];
+ if (tocHeadersArray[j][1] != "") {
+ commentText += "." + tocHeadersArray[j][1];
+ }
+ commentText += " " + (j + 1) + " ";
+ }
+ }
+ // important, we have to remove trailing spaces
+ commentText = TrimStringRight(commentText);
+
+ // forge a comment we'll insert in the TOC ; that comment will hold
+ // the TOC definition for us
+ var ct = theDocument.createComment(commentText);
+ toc.appendChild(ct);
+
+ // assign a special class to the TOC top element if the TOC is readonly
+ // the definition of this class is in EditorOverride.css
+ if (readonly) {
+ toc.setAttribute("class", "readonly");
+ } else {
+ toc.removeAttribute("class");
+ }
+
+ // We need a new variable to hold the local ul/ol container
+ // The toplevel TOC element is not the parent element of a
+ // TOC entry if its depth is > 1...
+ var tocList = toc;
+ // create a list item
+ var tocItem = theDocument.createElement("li");
+ // and an anchor in this list item
+ var tocAnchor = theDocument.createElement("a");
+ // make it target the source of the TOC entry
+ tocAnchor.setAttribute("href", "#" + tocArray[i + 2]);
+ // and put the textual contents of the TOC entry in that anchor
+ var tocEntry = theDocument.createTextNode(tocArray[i + 1]);
+ // now, insert everything where it has to be inserted
+ tocAnchor.appendChild(tocEntry);
+ tocItem.appendChild(tocAnchor);
+ tocList.appendChild(tocItem);
+ item = tocList;
+ } else {
+ if (tocArray[i] < headerIndex) {
+ // if the depth of the new TOC entry is less than the depth of the
+ // last entry we created, find the good ul/ol ancestor
+ for (j = headerIndex - tocArray[i]; j > 0; --j) {
+ if (item != toc) {
+ item = item.parentNode.parentNode;
+ }
+ }
+ tocItem = theDocument.createElement("li");
+ } else if (tocArray[i] > headerIndex) {
+ // to the contrary, it's deeper than the last one
+ // we need to create sub ul/ol's and li's
+ for (j = tocArray[i] - headerIndex; j > 0; --j) {
+ tocList = theDocument.createElement(orderedList ? "ol" : "ul");
+ item.lastChild.appendChild(tocList);
+ tocItem = theDocument.createElement("li");
+ tocList.appendChild(tocItem);
+ item = tocList;
+ }
+ } else {
+ tocItem = theDocument.createElement("li");
+ }
+ tocAnchor = theDocument.createElement("a");
+ tocAnchor.setAttribute("href", "#" + tocArray[i + 2]);
+ tocEntry = theDocument.createTextNode(tocArray[i + 1]);
+ tocAnchor.appendChild(tocEntry);
+ tocItem.appendChild(tocAnchor);
+ item.appendChild(tocItem);
+ headerIndex = tocArray[i];
+ }
+ }
+ SaveWindowLocation();
+}
+
+function selectHeader(elt, index) {
+ var tag = elt.value;
+ tocHeadersArray[index - 1][0] = tag;
+ var textbox = document.getElementById("header" + index + "Class");
+ if (tag == "") {
+ textbox.setAttribute("disabled", "true");
+ } else {
+ textbox.removeAttribute("disabled");
+ }
+}
+
+function changeClass(elt, index) {
+ tocHeadersArray[index - 1][1] = elt.value;
+}
+
+function ToggleReadOnlyToc(elt) {
+ readonly = elt.checked;
+}
+
+function ToggleOrderedList(elt) {
+ orderedList = elt.checked;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml
new file mode 100644
index 0000000000..38c85c764d
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml
@@ -0,0 +1,505 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertTOC.dtd">
+
+<window
+ title="&Window.title;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="Startup();"
+ lightweightthemes="true"
+ oncancel="window.close(); return true;"
+>
+ <dialog>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdInsertTOC.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+ <spacer id="dummy" style="display: none" />
+ <vbox flex="1">
+ <html:fieldset>
+ <html:legend>&buildToc.label;</html:legend>
+ <html:table>
+ <html:tr>
+ <html:th></html:th>
+ <html:th>&tag.label;</html:th>
+ <html:th>&class.label;</html:th>
+ </html:tr>
+ <html:tr>
+ <html:th id="header1Label">&header1.label;</html:th>
+ <html:td>
+ <menulist id="header1Menulist">
+ <menupopup>
+ <menuitem
+ id="header1none"
+ label="--"
+ value=""
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuseparator />
+ <menuitem
+ id="header1H1"
+ label="h1"
+ value="h1"
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuitem
+ id="header1H2"
+ label="h2"
+ value="h2"
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuitem
+ id="header1H3"
+ label="h3"
+ value="h3"
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuitem
+ id="header1H4"
+ label="h4"
+ value="h4"
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuitem
+ id="header1H5"
+ label="h5"
+ value="h5"
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuitem
+ id="header1H6"
+ label="h6"
+ value="h6"
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuitem
+ id="header1DIV"
+ label="div"
+ value="div"
+ oncommand="selectHeader(this, 1)"
+ />
+ <menuitem
+ id="header1P"
+ label="p"
+ value="p"
+ oncommand="selectHeader(this, 1)"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ <html:td>
+ <html:input
+ id="header1Class"
+ type="text"
+ class="input-inline"
+ size="10"
+ onchange="changeClass(this, 1)"
+ aria-labelledby="header1Label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="header2Label">&header2.label;</html:th>
+ <html:td>
+ <menulist id="header2Menulist">
+ <menupopup>
+ <menuitem
+ id="header2none"
+ label="--"
+ value=""
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuseparator />
+ <menuitem
+ id="header2H1"
+ label="h1"
+ value="h1"
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuitem
+ id="header2H2"
+ label="h2"
+ value="h2"
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuitem
+ id="header2H3"
+ label="h3"
+ value="h3"
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuitem
+ id="header2H4"
+ label="h4"
+ value="h4"
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuitem
+ id="header2H5"
+ label="h5"
+ value="h5"
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuitem
+ id="header2H6"
+ label="h6"
+ value="h6"
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuitem
+ id="header2DIV"
+ label="div"
+ value="div"
+ oncommand="selectHeader(this, 2)"
+ />
+ <menuitem
+ id="header2P"
+ label="p"
+ value="p"
+ oncommand="selectHeader(this, 2)"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ <html:td>
+ <html:input
+ id="header2Class"
+ type="text"
+ class="input-inline"
+ size="10"
+ onchange="changeClass(this, 2)"
+ aria-labelledby="header2Label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="header3Label">&header3.label;</html:th>
+ <html:td>
+ <menulist id="header3Menulist">
+ <menupopup>
+ <menuitem
+ id="header3none"
+ label="--"
+ value=""
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuseparator />
+ <menuitem
+ id="header3H1"
+ label="h1"
+ value="h1"
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuitem
+ id="header3H2"
+ label="h2"
+ value="h2"
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuitem
+ id="header3H3"
+ label="h3"
+ value="h3"
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuitem
+ id="header3H4"
+ label="h4"
+ value="h4"
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuitem
+ id="header3H5"
+ label="h5"
+ value="h5"
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuitem
+ id="header3H6"
+ label="h6"
+ value="h6"
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuitem
+ id="header3DIV"
+ label="div"
+ value="div"
+ oncommand="selectHeader(this, 3)"
+ />
+ <menuitem
+ id="header3P"
+ label="p"
+ value="p"
+ oncommand="selectHeader(this, 3)"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ <html:td>
+ <html:input
+ id="header3Class"
+ type="text"
+ class="input-inline"
+ size="10"
+ onchange="changeClass(this, 3)"
+ aria-labelledby="header3Label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="header4Label">&header4.label;</html:th>
+ <html:td>
+ <menulist id="header4Menulist">
+ <menupopup>
+ <menuitem
+ id="header4none"
+ label="--"
+ value=""
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuseparator />
+ <menuitem
+ id="header4H1"
+ label="h1"
+ value="h1"
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuitem
+ id="header4H2"
+ label="h2"
+ value="h2"
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuitem
+ id="header4H3"
+ label="h3"
+ value="h3"
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuitem
+ id="header4H4"
+ label="h4"
+ value="h4"
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuitem
+ id="header4H5"
+ label="h5"
+ value="h5"
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuitem
+ id="header4H6"
+ label="h6"
+ value="h6"
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuitem
+ id="header4DIV"
+ label="div"
+ value="div"
+ oncommand="selectHeader(this, 4)"
+ />
+ <menuitem
+ id="header4P"
+ label="p"
+ value="p"
+ oncommand="selectHeader(this, 4)"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ <html:td>
+ <html:input
+ id="header4Class"
+ type="text"
+ class="input-inline"
+ size="10"
+ onchange="changeClass(this, 4)"
+ aria-labelledby="header4Label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="header5Label">&header5.label;</html:th>
+ <html:td>
+ <menulist id="header5Menulist">
+ <menupopup>
+ <menuitem
+ id="header5none"
+ label="--"
+ value=""
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuseparator />
+ <menuitem
+ id="header5H1"
+ label="h1"
+ value="h1"
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuitem
+ id="header5H2"
+ label="h2"
+ value="h2"
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuitem
+ id="header5H3"
+ label="h3"
+ value="h3"
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuitem
+ id="header5H4"
+ label="h4"
+ value="h4"
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuitem
+ id="header5H5"
+ label="h5"
+ value="h5"
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuitem
+ id="header5H6"
+ label="h6"
+ value="h6"
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuitem
+ id="header5DIV"
+ label="div"
+ value="div"
+ oncommand="selectHeader(this, 5)"
+ />
+ <menuitem
+ id="header5P"
+ label="p"
+ value="p"
+ oncommand="selectHeader(this, 5)"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ <html:td>
+ <html:input
+ id="header5Class"
+ type="text"
+ class="input-inline"
+ size="10"
+ onchange="changeClass(this, 5)"
+ aria-labelledby="header5Label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="header6Label">&header6.label;</html:th>
+ <html:td>
+ <menulist id="header6Menulist">
+ <menupopup>
+ <menuitem
+ id="header6none"
+ label="--"
+ value=""
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuseparator />
+ <menuitem
+ id="header6H1"
+ label="h1"
+ value="h1"
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuitem
+ id="header6H2"
+ label="h2"
+ value="h2"
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuitem
+ id="header6H3"
+ label="h3"
+ value="h3"
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuitem
+ id="header6H4"
+ label="h4"
+ value="h4"
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuitem
+ id="header6H5"
+ label="h5"
+ value="h5"
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuitem
+ id="header6H6"
+ label="h6"
+ value="h6"
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuitem
+ id="header6DIV"
+ label="div"
+ value="div"
+ oncommand="selectHeader(this, 6)"
+ />
+ <menuitem
+ id="header6P"
+ label="p"
+ value="p"
+ oncommand="selectHeader(this, 6)"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ <html:td>
+ <html:input
+ id="header6Class"
+ type="text"
+ class="input-inline"
+ size="10"
+ onchange="changeClass(this, 6)"
+ aria-labelledby="header6Label"
+ />
+ </html:td>
+ </html:tr>
+ </html:table>
+ </html:fieldset>
+ <vbox>
+ <checkbox
+ id="orderedListCheckbox"
+ label="&orderedList.label;"
+ oncommand="ToggleOrderedList(this)"
+ />
+ <checkbox
+ id="readOnlyCheckbox"
+ label="&makeReadOnly.label;"
+ oncommand="ToggleReadOnlyToc(this)"
+ />
+ </vbox>
+ <separator class="groove" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTable.js b/comm/mail/components/compose/content/dialogs/EdInsertTable.js
new file mode 100644
index 0000000000..5da0da46d3
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertTable.js
@@ -0,0 +1,258 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+// Cancel() is in EdDialogCommon.js
+
+var gTableElement = null;
+var gRows;
+var gColumns;
+var gActiveEditor;
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ gActiveEditor = GetCurrentTableEditor();
+ if (!gActiveEditor) {
+ dump("Failed to get active editor!\n");
+ window.close();
+ return;
+ }
+
+ try {
+ gTableElement = gActiveEditor.createElementWithDefaults("table");
+ } catch (e) {}
+
+ if (!gTableElement) {
+ dump("Failed to create a new table!\n");
+ window.close();
+ return;
+ }
+ gDialog.rowsInput = document.getElementById("rowsInput");
+ gDialog.columnsInput = document.getElementById("columnsInput");
+ gDialog.widthInput = document.getElementById("widthInput");
+ gDialog.borderInput = document.getElementById("borderInput");
+ gDialog.widthPixelOrPercentMenulist = document.getElementById(
+ "widthPixelOrPercentMenulist"
+ );
+ gDialog.OkButton = document.querySelector("dialog").getButton("accept");
+
+ // Make a copy to use for AdvancedEdit
+ globalElement = gTableElement.cloneNode(false);
+ try {
+ if (
+ Services.prefs.getBoolPref("editor.use_css") &&
+ IsHTMLEditor() &&
+ !(gActiveEditor.flags & Ci.nsIEditor.eEditorMailMask)
+ ) {
+ // only for Composer and not for htmlmail
+ globalElement.setAttribute("style", "text-align: left;");
+ }
+ } catch (e) {}
+
+ // Initialize all widgets with image attributes
+ InitDialog();
+
+ // Set initial number to 2 rows, 2 columns:
+ // Note, these are not attributes on the table,
+ // so don't put them in InitDialog(),
+ // else the user's values will be trashed when they use
+ // the Advanced Edit dialog
+ gDialog.rowsInput.value = 2;
+ gDialog.columnsInput.value = 2;
+
+ // If no default value on the width, set to 100%
+ if (gDialog.widthInput.value.length == 0) {
+ gDialog.widthInput.value = "100";
+ gDialog.widthPixelOrPercentMenulist.selectedIndex = 1;
+ }
+
+ SetTextboxFocusById("rowsInput");
+
+ SetWindowLocation();
+}
+
+// Set dialog widgets with attribute data
+// We get them from globalElement copy so this can be used
+// by AdvancedEdit(), which is shared by all property dialogs
+function InitDialog() {
+ // Get default attributes set on the created table:
+ // Get the width attribute of the element, stripping out "%"
+ // This sets contents of menu combobox list
+ // 2nd param = null: Use current selection to find if parent is table cell or window
+ gDialog.widthInput.value = InitPixelOrPercentMenulist(
+ globalElement,
+ null,
+ "width",
+ "widthPixelOrPercentMenulist",
+ gPercent
+ );
+ gDialog.borderInput.value = globalElement.getAttribute("border");
+}
+
+function ChangeRowOrColumn(id) {
+ // Allow only integers
+ forceInteger(id);
+
+ // Enable OK only if both rows and columns have a value > 0
+ var enable =
+ gDialog.rowsInput.value.length > 0 &&
+ gDialog.rowsInput.value > 0 &&
+ gDialog.columnsInput.value.length > 0 &&
+ gDialog.columnsInput.value > 0;
+
+ SetElementEnabled(gDialog.OkButton, enable);
+ SetElementEnabledById("AdvancedEditButton1", enable);
+}
+
+// Get and validate data from widgets.
+// Set attributes on globalElement so they can be accessed by AdvancedEdit()
+function ValidateData() {
+ gRows = ValidateNumber(
+ gDialog.rowsInput,
+ null,
+ 1,
+ gMaxRows,
+ null,
+ null,
+ true
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ gColumns = ValidateNumber(
+ gDialog.columnsInput,
+ null,
+ 1,
+ gMaxColumns,
+ null,
+ null,
+ true
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ // Set attributes: NOTE: These may be empty strings (last param = false)
+ ValidateNumber(
+ gDialog.borderInput,
+ null,
+ 0,
+ gMaxPixels,
+ globalElement,
+ "border",
+ false
+ );
+ // TODO: Deal with "BORDER" without value issue
+ if (gValidationError) {
+ return false;
+ }
+
+ ValidateNumber(
+ gDialog.widthInput,
+ gDialog.widthPixelOrPercentMenulist,
+ 1,
+ gMaxTableSize,
+ globalElement,
+ "width",
+ false
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ return true;
+}
+
+function onAccept(event) {
+ if (ValidateData()) {
+ gActiveEditor.beginTransaction();
+ try {
+ gActiveEditor.cloneAttributes(gTableElement, globalElement);
+
+ // Create necessary rows and cells for the table
+ var tableBody = gActiveEditor.createElementWithDefaults("tbody");
+ if (tableBody) {
+ gTableElement.appendChild(tableBody);
+
+ // Create necessary rows and cells for the table
+ for (var i = 0; i < gRows; i++) {
+ var newRow = gActiveEditor.createElementWithDefaults("tr");
+ if (newRow) {
+ tableBody.appendChild(newRow);
+ for (var j = 0; j < gColumns; j++) {
+ var newCell = gActiveEditor.createElementWithDefaults("td");
+ if (newCell) {
+ newRow.appendChild(newCell);
+ }
+ }
+ }
+ }
+ }
+ // Detect when entire cells are selected:
+ // Get number of cells selected
+ var tagNameObj = { value: "" };
+ var countObj = { value: 0 };
+ var element = gActiveEditor.getSelectedOrParentTableElement(
+ tagNameObj,
+ countObj
+ );
+ var deletePlaceholder = false;
+
+ if (tagNameObj.value == "table") {
+ // Replace entire selected table with new table, so delete the table
+ gActiveEditor.deleteTable();
+ } else if (tagNameObj.value == "td") {
+ if (countObj.value >= 1) {
+ if (countObj.value > 1) {
+ // Assume user wants to replace a block of
+ // contiguous cells with a table, so
+ // join the selected cells
+ gActiveEditor.joinTableCells(false);
+
+ // Get the cell everything was merged into
+ element = gActiveEditor.getSelectedCells()[0];
+
+ // Collapse selection into just that cell
+ gActiveEditor.selection.collapse(element, 0);
+ }
+
+ if (element) {
+ // Empty just the contents of the cell
+ gActiveEditor.deleteTableCellContents();
+
+ // Collapse selection to start of empty cell...
+ gActiveEditor.selection.collapse(element, 0);
+ // ...but it will contain a <br> placeholder
+ deletePlaceholder = true;
+ }
+ }
+ }
+
+ // true means delete selection when inserting
+ gActiveEditor.insertElementAtSelection(gTableElement, true);
+
+ if (
+ deletePlaceholder &&
+ gTableElement &&
+ gTableElement.nextElementSibling
+ ) {
+ // Delete the placeholder <br>
+ gActiveEditor.deleteNode(gTableElement.nextElementSibling);
+ }
+ } catch (e) {}
+
+ gActiveEditor.endTransaction();
+
+ SaveWindowLocation();
+ return;
+ }
+ event.preventDefault();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml
new file mode 100644
index 0000000000..b114e09d44
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml
@@ -0,0 +1,126 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % edInsertTable SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertTable.dtd">
+%edInsertTable;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog>
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdInsertTable.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+ <html:table>
+ <html:tr>
+ <html:th>
+ <label
+ control="rowsInput"
+ value="&numRowsEditField.label;"
+ accesskey="&numRowsEditField.accessKey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="rowsInput"
+ type="number"
+ class="narrow input-inline"
+ oninput="ChangeRowOrColumn(this.id)"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ control="columnsInput"
+ value="&numColumnsEditField.label;"
+ accesskey="&numColumnsEditField.accessKey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="columnsInput"
+ type="number"
+ class="narrow input-inline"
+ oninput="ChangeRowOrColumn(this.id)"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ control="widthInput"
+ value="&widthEditField.label;"
+ accesskey="&widthEditField.accessKey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="widthInput"
+ type="number"
+ class="narrow input-inline"
+ oninput="forceInteger(this.id)"
+ />
+ </html:td>
+ <html:td>
+ <menulist id="widthPixelOrPercentMenulist" class="menulist-narrow" />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ control="borderInput"
+ value="&borderEditField.label;"
+ accesskey="&borderEditField.accessKey;"
+ tooltiptext="&borderEditField.tooltip;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="borderInput"
+ type="number"
+ class="narrow input-inline"
+ oninput="forceInteger(this.id)"
+ />
+ </html:td>
+ <html:td>
+ <label value="&pixels.label;" />
+ </html:td>
+ </html:tr>
+ </html:table>
+ <vbox id="AdvancedEdit">
+ <hbox flex="1" style="margin-top: 0.2em" align="center">
+ <!-- This will right-align the button -->
+ <spacer flex="1" />
+ <button
+ id="AdvancedEditButton1"
+ oncommand="onAdvancedEdit()"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <separator id="advancedSeparator" class="groove" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdLinkProps.js b/comm/mail/components/compose/content/dialogs/EdLinkProps.js
new file mode 100644
index 0000000000..903a4d3099
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdLinkProps.js
@@ -0,0 +1,323 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var gActiveEditor;
+var anchorElement = null;
+var imageElement = null;
+var insertNew = false;
+var replaceExistingLink = false;
+var insertLinkAtCaret;
+var needLinkText = false;
+var href;
+var newLinkText;
+var gHNodeArray = {};
+var gHaveNamedAnchors = false;
+var gHaveHeadings = false;
+var gCanChangeHeadingSelected = true;
+var gCanChangeAnchorSelected = true;
+
+// NOTE: Use "href" instead of "a" to distinguish from Named Anchor
+// The returned node is has an "a" tagName
+var tagName = "href";
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ gActiveEditor = GetCurrentEditor();
+ if (!gActiveEditor) {
+ dump("Failed to get active editor!\n");
+ window.close();
+ return;
+ }
+ // Message was wrapped in a <label> or <div>, so actual text is a child text node
+ gDialog.linkTextCaption = document.getElementById("linkTextCaption");
+ gDialog.linkTextMessage = document.getElementById("linkTextMessage");
+ gDialog.linkTextInput = document.getElementById("linkTextInput");
+ gDialog.hrefInput = document.getElementById("hrefInput");
+ gDialog.makeRelativeLink = document.getElementById("MakeRelativeLink");
+ gDialog.AdvancedEditSection = document.getElementById("AdvancedEdit");
+
+ // See if we have a single selected image
+ imageElement = gActiveEditor.getSelectedElement("img");
+
+ if (imageElement) {
+ // Get the parent link if it exists -- more efficient than GetSelectedElement()
+ anchorElement = gActiveEditor.getElementOrParentByTagName(
+ "href",
+ imageElement
+ );
+ if (anchorElement) {
+ if (anchorElement.children.length > 1) {
+ // If there are other children, then we want to break
+ // this image away by inserting a new link around it,
+ // so make a new node and copy existing attributes
+ anchorElement = anchorElement.cloneNode(false);
+ // insertNew = true;
+ replaceExistingLink = true;
+ }
+ }
+ } else {
+ // Get an anchor element if caret or
+ // entire selection is within the link.
+ anchorElement = gActiveEditor.getSelectedElement(tagName);
+
+ if (anchorElement) {
+ // Select the entire link
+ gActiveEditor.selectElement(anchorElement);
+ } else {
+ // If selection starts in a link, but extends beyond it,
+ // the user probably wants to extend existing link to new selection,
+ // so check if either end of selection is within a link
+ // POTENTIAL PROBLEM: This prevents user from selecting text in an existing
+ // link and making 2 links.
+ // Note that this isn't a problem with images, handled above
+
+ anchorElement = gActiveEditor.getElementOrParentByTagName(
+ "href",
+ gActiveEditor.selection.anchorNode
+ );
+ if (!anchorElement) {
+ anchorElement = gActiveEditor.getElementOrParentByTagName(
+ "href",
+ gActiveEditor.selection.focusNode
+ );
+ }
+
+ if (anchorElement) {
+ // But clone it for reinserting/merging around existing
+ // link that only partially overlaps the selection
+ anchorElement = anchorElement.cloneNode(false);
+ // insertNew = true;
+ replaceExistingLink = true;
+ }
+ }
+ }
+
+ if (!anchorElement) {
+ // No existing link -- create a new one
+ anchorElement = gActiveEditor.createElementWithDefaults(tagName);
+ insertNew = true;
+ // Hide message about removing existing link
+ // document.getElementById("RemoveLinkMsg").hidden = true;
+ }
+ if (!anchorElement) {
+ dump("Failed to get selected element or create a new one!\n");
+ window.close();
+ return;
+ }
+
+ // We insert at caret only when nothing is selected
+ insertLinkAtCaret = gActiveEditor.selection.isCollapsed;
+
+ var selectedText;
+ if (insertLinkAtCaret) {
+ // Groupbox caption:
+ gDialog.linkTextCaption.setAttribute("label", GetString("LinkText"));
+
+ // Message above input field:
+ gDialog.linkTextMessage.setAttribute("value", GetString("EnterLinkText"));
+ gDialog.linkTextMessage.setAttribute(
+ "accesskey",
+ GetString("EnterLinkTextAccessKey")
+ );
+ } else {
+ if (!imageElement) {
+ // We get here if selection is exactly around a link node
+ // Check if selection has some text - use that first
+ selectedText = GetSelectionAsText();
+ if (!selectedText) {
+ // No text, look for first image in the selection
+ imageElement = anchorElement.querySelector("img");
+ }
+ }
+ // Set "caption" for link source and the source text or image URL
+ if (imageElement) {
+ gDialog.linkTextCaption.setAttribute("label", GetString("LinkImage"));
+ // Link source string is the source URL of image
+ // TODO: THIS DOESN'T HANDLE MULTIPLE SELECTED IMAGES!
+ gDialog.linkTextMessage.setAttribute("value", imageElement.src);
+ } else {
+ gDialog.linkTextCaption.setAttribute("label", GetString("LinkText"));
+ if (selectedText) {
+ // Use just the first 60 characters and add "..."
+ gDialog.linkTextMessage.setAttribute(
+ "value",
+ TruncateStringAtWordEnd(
+ ReplaceWhitespace(selectedText, " "),
+ 60,
+ true
+ )
+ );
+ } else {
+ gDialog.linkTextMessage.setAttribute(
+ "value",
+ GetString("MixedSelection")
+ );
+ }
+ }
+ }
+
+ // Make a copy to use for AdvancedEdit and onSaveDefault
+ globalElement = anchorElement.cloneNode(false);
+
+ // Get the list of existing named anchors and headings
+ FillLinkMenulist(gDialog.hrefInput, gHNodeArray);
+
+ // We only need to test for this once per dialog load
+ gHaveDocumentUrl = GetDocumentBaseUrl();
+
+ // Set data for the dialog controls
+ InitDialog();
+
+ // Search for a URI pattern in the selected text
+ // as candidate href
+ selectedText = TrimString(selectedText);
+ if (!gDialog.hrefInput.value && TextIsURI(selectedText)) {
+ gDialog.hrefInput.value = selectedText;
+ }
+
+ // Set initial focus
+ if (insertLinkAtCaret) {
+ // We will be using the HREF inputbox, so text message
+ gDialog.linkTextInput.focus();
+ } else {
+ gDialog.hrefInput.select();
+ gDialog.hrefInput.focus();
+
+ // We will not insert a new link at caret, so remove link text input field
+ gDialog.linkTextInput.hidden = true;
+ gDialog.linkTextInput = null;
+ }
+
+ // This sets enable state on OK button
+ doEnabling();
+
+ SetWindowLocation();
+}
+
+// Set dialog widgets with attribute data
+// We get them from globalElement copy so this can be used
+// by AdvancedEdit(), which is shared by all property dialogs
+function InitDialog() {
+ // Must use getAttribute, not "globalElement.href",
+ // or foreign chars aren't converted correctly!
+ gDialog.hrefInput.value = globalElement.getAttribute("href");
+
+ // Set "Relativize" checkbox according to current URL state
+ SetRelativeCheckbox(gDialog.makeRelativeLink);
+}
+
+function doEnabling() {
+ // We disable Ok button when there's no href text only if inserting a new link
+ var enable = insertNew
+ ? TrimString(gDialog.hrefInput.value).length > 0
+ : true;
+
+ // anon. content, so can't use SetElementEnabledById here
+ var dialogNode = document.getElementById("linkDlg");
+ dialogNode.getButton("accept").disabled = !enable;
+
+ SetElementEnabledById("AdvancedEditButton1", enable);
+}
+
+function ChangeLinkLocation() {
+ SetRelativeCheckbox(gDialog.makeRelativeLink);
+ // Set OK button enable state
+ doEnabling();
+}
+
+// Get and validate data from widgets.
+// Set attributes on globalElement so they can be accessed by AdvancedEdit()
+function ValidateData() {
+ href = TrimString(gDialog.hrefInput.value);
+ if (href) {
+ // Set the HREF directly on the editor document's anchor node
+ // or on the newly-created node if insertNew is true
+ globalElement.setAttribute("href", href);
+ } else if (insertNew) {
+ // We must have a URL to insert a new link
+ // NOTE: We accept an empty HREF on existing link to indicate removing the link
+ ShowInputErrorMessage(GetString("EmptyHREFError"));
+ return false;
+ }
+ if (gDialog.linkTextInput) {
+ // The text we will insert isn't really an attribute,
+ // but it makes sense to validate it
+ newLinkText = TrimString(gDialog.linkTextInput.value);
+ if (!newLinkText) {
+ if (href) {
+ newLinkText = href;
+ } else {
+ ShowInputErrorMessage(GetString("EmptyLinkTextError"));
+ SetTextboxFocus(gDialog.linkTextInput);
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+function onAccept(event) {
+ if (ValidateData()) {
+ if (href.length > 0) {
+ // Copy attributes to element we are changing or inserting
+ gActiveEditor.cloneAttributes(anchorElement, globalElement);
+
+ // Coalesce into one undo transaction
+ gActiveEditor.beginTransaction();
+
+ // Get text to use for a new link
+ if (insertLinkAtCaret) {
+ // Append the link text as the last child node
+ // of the anchor node
+ var textNode = gActiveEditor.document.createTextNode(newLinkText);
+ if (textNode) {
+ anchorElement.appendChild(textNode);
+ }
+ try {
+ gActiveEditor.insertElementAtSelection(anchorElement, false);
+ } catch (e) {
+ dump("Exception occurred in InsertElementAtSelection\n");
+ return;
+ }
+ } else if (insertNew || replaceExistingLink) {
+ // Link source was supplied by the selection,
+ // so insert a link node as parent of this
+ // (may be text, image, or other inline content)
+ try {
+ gActiveEditor.insertLinkAroundSelection(anchorElement);
+ } catch (e) {
+ dump("Exception occurred in InsertElementAtSelection\n");
+ return;
+ }
+ }
+ // Check if the link was to a heading
+ if (href in gHNodeArray) {
+ var anchorNode = gActiveEditor.createElementWithDefaults("a");
+ if (anchorNode) {
+ anchorNode.name = href.substr(1);
+
+ // Insert the anchor into the document,
+ // but don't let the transaction change the selection
+ gActiveEditor.setShouldTxnSetSelection(false);
+ gActiveEditor.insertNode(anchorNode, gHNodeArray[href], 0);
+ gActiveEditor.setShouldTxnSetSelection(true);
+ }
+ }
+ gActiveEditor.endTransaction();
+ } else if (!insertNew) {
+ // We already had a link, but empty HREF means remove it
+ EditorRemoveTextProperty("href", "");
+ }
+ SaveWindowLocation();
+ return;
+ }
+ event.preventDefault();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml b/comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml
new file mode 100644
index 0000000000..7c550a7a45
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % linkPropertiesDTD SYSTEM "chrome://messenger/locale/messengercompose/EditorLinkProperties.dtd">
+%linkPropertiesDTD;
+<!ENTITY % composeEditorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/mailComposeEditorOverlay.dtd">
+%composeEditorOverlayDTD;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+ style="min-height: 26em"
+>
+ <dialog id="linkDlg" style="width: 50ch">
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdLinkProps.js" />
+ <script src="chrome://messenger/content/messengercompose/EdImageLinkLoader.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <vbox>
+ <html:fieldset>
+ <html:legend><label id="linkTextCaption" /></html:legend>
+ <vbox>
+ <label id="linkTextMessage" control="linkTextInput" />
+ <html:input
+ id="linkTextInput"
+ type="text"
+ class="input-inline"
+ aria-labelledby="linkTextMessage"
+ />
+ </vbox>
+ </html:fieldset>
+
+ <html:fieldset id="LinkURLBox">
+ <html:legend>&LinkURLBox.label;</html:legend>
+ <vbox id="LinkLocationBox">
+ <label
+ id="hrefLabel"
+ control="hrefInput"
+ accesskey="&LinkURLEditField2.accessKey;"
+ width="1"
+ >&LinkURLEditField2.label;</label
+ >
+ <html:input
+ id="hrefInput"
+ type="text"
+ class="input-inline uri-element padded"
+ oninput="ChangeLinkLocation();"
+ aria-labelledby="hrefLabel"
+ />
+ <hbox align="center">
+ <checkbox
+ id="MakeRelativeLink"
+ for="hrefInput"
+ label="&makeUrlRelative.label;"
+ accesskey="&makeUrlRelative.accessKey;"
+ oncommand="MakeInputValueRelativeOrAbsolute(this);"
+ tooltiptext="&makeUrlRelative.tooltip;"
+ />
+ <spacer flex="1" />
+ <button
+ label="&chooseFileLinkButton.label;"
+ accesskey="&chooseFileLinkButton.accessKey;"
+ oncommand="chooseLinkFile();"
+ />
+ </hbox>
+ </vbox>
+ <checkbox
+ id="AttachSourceToMail"
+ hidden="true"
+ label="&attachLinkSource.label;"
+ accesskey="&attachLinkSource.accesskey;"
+ oncommand="DoAttachSourceCheckbox()"
+ />
+ </html:fieldset>
+ </vbox>
+ <vbox id="AdvancedEdit">
+ <hbox flex="1" style="margin-top: 0.2em" align="center">
+ <!-- This will right-align the button -->
+ <spacer flex="1" />
+ <button
+ id="AdvancedEditButton1"
+ oncommand="onAdvancedEdit()"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <separator id="advancedSeparator" class="groove" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdListProps.js b/comm/mail/components/compose/content/dialogs/EdListProps.js
new file mode 100644
index 0000000000..c33efc9bb1
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdListProps.js
@@ -0,0 +1,455 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+// Cancel() is in EdDialogCommon.js
+var gBulletStyleType = "";
+var gNumberStyleType = "";
+var gListElement;
+var gOriginalListType = "";
+var gListType = "";
+var gMixedListSelection = false;
+var gStyleType = "";
+var gOriginalStyleType = "";
+const gOnesArray = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"];
+const gTensArray = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"];
+const gHundredsArray = [
+ "",
+ "C",
+ "CC",
+ "CCC",
+ "CD",
+ "D",
+ "DC",
+ "DCC",
+ "DCCC",
+ "CM",
+];
+const gThousandsArray = [
+ "",
+ "M",
+ "MM",
+ "MMM",
+ "MMMM",
+ "MMMMM",
+ "MMMMMM",
+ "MMMMMMM",
+ "MMMMMMMM",
+ "MMMMMMMMM",
+];
+const gRomanDigits = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 };
+const A = "A".charCodeAt(0);
+const gArabic = "1";
+const gUpperRoman = "I";
+const gLowerRoman = "i";
+const gUpperLetters = "A";
+const gLowerLetters = "a";
+const gDecimalCSS = "decimal";
+const gUpperRomanCSS = "upper-roman";
+const gLowerRomanCSS = "lower-roman";
+const gUpperAlphaCSS = "upper-alpha";
+const gLowerAlphaCSS = "lower-alpha";
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+ gDialog.ListTypeList = document.getElementById("ListType");
+ gDialog.BulletStyleList = document.getElementById("BulletStyle");
+ gDialog.BulletStyleLabel = document.getElementById("BulletStyleLabel");
+ gDialog.StartingNumberInput = document.getElementById("StartingNumber");
+ gDialog.StartingNumberLabel = document.getElementById("StartingNumberLabel");
+ gDialog.AdvancedEditButton = document.getElementById("AdvancedEditButton1");
+ gDialog.RadioGroup = document.getElementById("RadioGroup");
+ gDialog.ChangeAllRadio = document.getElementById("ChangeAll");
+ gDialog.ChangeSelectedRadio = document.getElementById("ChangeSelected");
+
+ // Try to get an existing list(s)
+ var mixedObj = { value: null };
+ try {
+ gListType = editor.getListState(mixedObj, {}, {}, {});
+
+ // We may have mixed list and non-list, or > 1 list type in selection
+ gMixedListSelection = mixedObj.value;
+
+ // Get the list element at the anchor node
+ gListElement = editor.getElementOrParentByTagName("list", null);
+ } catch (e) {}
+
+ // The copy to use in AdvancedEdit
+ if (gListElement) {
+ globalElement = gListElement.cloneNode(false);
+ }
+
+ // Show extra options for changing entire list if we have one already.
+ gDialog.RadioGroup.collapsed = !gListElement;
+ if (gListElement) {
+ // Radio button index is persistent
+ if (gDialog.RadioGroup.getAttribute("index") == "1") {
+ gDialog.RadioGroup.selectedItem = gDialog.ChangeSelectedRadio;
+ } else {
+ gDialog.RadioGroup.selectedItem = gDialog.ChangeAllRadio;
+ }
+ }
+
+ InitDialog();
+
+ gOriginalListType = gListType;
+
+ gDialog.ListTypeList.focus();
+
+ SetWindowLocation();
+}
+
+function InitDialog() {
+ // Note that if mixed, we we pay attention
+ // only to the anchor node's list type
+ // (i.e., don't confuse user with "mixed" designation)
+ if (gListElement) {
+ gListType = gListElement.nodeName.toLowerCase();
+ } else {
+ gListType = "";
+ }
+
+ gDialog.ListTypeList.value = gListType;
+ gDialog.StartingNumberInput.value = "";
+
+ // Last param = true means attribute value is case-sensitive
+ var type = globalElement
+ ? GetHTMLOrCSSStyleValue(globalElement, "type", "list-style-type")
+ : null;
+
+ if (gListType == "ul") {
+ if (type) {
+ type = type.toLowerCase();
+ gBulletStyleType = type;
+ gOriginalStyleType = type;
+ }
+ } else if (gListType == "ol") {
+ // Translate CSS property strings
+ switch (type.toLowerCase()) {
+ case gDecimalCSS:
+ type = gArabic;
+ break;
+ case gUpperRomanCSS:
+ type = gUpperRoman;
+ break;
+ case gLowerRomanCSS:
+ type = gLowerRoman;
+ break;
+ case gUpperAlphaCSS:
+ type = gUpperLetters;
+ break;
+ case gLowerAlphaCSS:
+ type = gLowerLetters;
+ break;
+ }
+ if (type) {
+ gNumberStyleType = type;
+ gOriginalStyleType = type;
+ }
+
+ // Convert attribute number to appropriate letter or roman numeral
+ gDialog.StartingNumberInput.value = ConvertStartAttrToUserString(
+ globalElement.getAttribute("start"),
+ type
+ );
+ }
+ BuildBulletStyleList();
+}
+
+// Convert attribute number to appropriate letter or roman numeral
+function ConvertStartAttrToUserString(startAttr, type) {
+ switch (type) {
+ case gUpperRoman:
+ startAttr = ConvertArabicToRoman(startAttr);
+ break;
+ case gLowerRoman:
+ startAttr = ConvertArabicToRoman(startAttr).toLowerCase();
+ break;
+ case gUpperLetters:
+ startAttr = ConvertArabicToLetters(startAttr);
+ break;
+ case gLowerLetters:
+ startAttr = ConvertArabicToLetters(startAttr).toLowerCase();
+ break;
+ }
+ return startAttr;
+}
+
+function BuildBulletStyleList() {
+ gDialog.BulletStyleList.removeAllItems();
+ var label;
+
+ if (gListType == "ul") {
+ gDialog.BulletStyleList.removeAttribute("disabled");
+ gDialog.BulletStyleLabel.removeAttribute("disabled");
+ gDialog.StartingNumberInput.setAttribute("disabled", "true");
+ gDialog.StartingNumberLabel.setAttribute("disabled", "true");
+
+ label = GetString("BulletStyle");
+
+ gDialog.BulletStyleList.appendItem(GetString("Automatic"), "");
+ gDialog.BulletStyleList.appendItem(GetString("SolidCircle"), "disc");
+ gDialog.BulletStyleList.appendItem(GetString("OpenCircle"), "circle");
+ gDialog.BulletStyleList.appendItem(GetString("SolidSquare"), "square");
+
+ gDialog.BulletStyleList.value = gBulletStyleType;
+ } else if (gListType == "ol") {
+ gDialog.BulletStyleList.removeAttribute("disabled");
+ gDialog.BulletStyleLabel.removeAttribute("disabled");
+ gDialog.StartingNumberInput.removeAttribute("disabled");
+ gDialog.StartingNumberLabel.removeAttribute("disabled");
+ label = GetString("NumberStyle");
+
+ gDialog.BulletStyleList.appendItem(GetString("Automatic"), "");
+ gDialog.BulletStyleList.appendItem(GetString("Style_1"), gArabic);
+ gDialog.BulletStyleList.appendItem(GetString("Style_I"), gUpperRoman);
+ gDialog.BulletStyleList.appendItem(GetString("Style_i"), gLowerRoman);
+ gDialog.BulletStyleList.appendItem(GetString("Style_A"), gUpperLetters);
+ gDialog.BulletStyleList.appendItem(GetString("Style_a"), gLowerLetters);
+
+ gDialog.BulletStyleList.value = gNumberStyleType;
+ } else {
+ gDialog.BulletStyleList.setAttribute("disabled", "true");
+ gDialog.BulletStyleLabel.setAttribute("disabled", "true");
+ gDialog.StartingNumberInput.setAttribute("disabled", "true");
+ gDialog.StartingNumberLabel.setAttribute("disabled", "true");
+ }
+
+ // Disable advanced edit button if changing to "normal"
+ if (gListType) {
+ gDialog.AdvancedEditButton.removeAttribute("disabled");
+ } else {
+ gDialog.AdvancedEditButton.setAttribute("disabled", "true");
+ }
+
+ if (label) {
+ gDialog.BulletStyleLabel.textContent = label;
+ }
+}
+
+function SelectListType() {
+ // Each list type is stored in the "value" of each menuitem
+ var NewType = gDialog.ListTypeList.value;
+
+ if (NewType == "ol") {
+ SetTextboxFocus(gDialog.StartingNumberInput);
+ }
+
+ if (gListType != NewType) {
+ gListType = NewType;
+
+ // Create a newlist object for Advanced Editing
+ try {
+ if (gListType) {
+ globalElement = GetCurrentEditor().createElementWithDefaults(gListType);
+ }
+ } catch (e) {}
+
+ BuildBulletStyleList();
+ }
+}
+
+function SelectBulletStyle() {
+ // Save the selected index so when user changes
+ // list style, restore index to associated list
+ // Each bullet or number type is stored in the "value" of each menuitem
+ if (gListType == "ul") {
+ gBulletStyleType = gDialog.BulletStyleList.value;
+ } else if (gListType == "ol") {
+ var type = gDialog.BulletStyleList.value;
+ if (gNumberStyleType != type) {
+ // Convert existing input value to attr number first,
+ // then convert to the appropriate format for the newly-selected
+ gDialog.StartingNumberInput.value = ConvertStartAttrToUserString(
+ ConvertUserStringToStartAttr(gNumberStyleType),
+ type
+ );
+
+ gNumberStyleType = type;
+ SetTextboxFocus(gDialog.StartingNumberInput);
+ }
+ }
+}
+
+function ValidateData() {
+ gBulletStyleType = gDialog.BulletStyleList.value;
+ // globalElement should already be of the correct type
+
+ if (globalElement) {
+ var editor = GetCurrentEditor();
+ if (gListType == "ul") {
+ if (gBulletStyleType && gDialog.ChangeAllRadio.selected) {
+ globalElement.setAttribute("type", gBulletStyleType);
+ } else {
+ try {
+ editor.removeAttributeOrEquivalent(globalElement, "type", true);
+ } catch (e) {}
+ }
+ } else if (gListType == "ol") {
+ if (gBulletStyleType) {
+ globalElement.setAttribute("type", gBulletStyleType);
+ } else {
+ try {
+ editor.removeAttributeOrEquivalent(globalElement, "type", true);
+ } catch (e) {}
+ }
+
+ var startingNumber = ConvertUserStringToStartAttr(gBulletStyleType);
+ if (startingNumber) {
+ globalElement.setAttribute("start", startingNumber);
+ } else {
+ globalElement.removeAttribute("start");
+ }
+ }
+ }
+ return true;
+}
+
+function ConvertUserStringToStartAttr(type) {
+ var startingNumber = TrimString(gDialog.StartingNumberInput.value);
+
+ switch (type) {
+ case gUpperRoman:
+ case gLowerRoman:
+ // If the input isn't an integer, assume it's a roman numeral. Convert it.
+ if (!Number(startingNumber)) {
+ startingNumber = ConvertRomanToArabic(startingNumber);
+ }
+ break;
+ case gUpperLetters:
+ case gLowerLetters:
+ // Get the number equivalent of the letters
+ if (!Number(startingNumber)) {
+ startingNumber = ConvertLettersToArabic(startingNumber);
+ }
+ break;
+ }
+ return startingNumber;
+}
+
+function ConvertRomanToArabic(num) {
+ num = num.toUpperCase();
+ if (num && !/[^MDCLXVI]/i.test(num)) {
+ var Arabic = 0;
+ var last_digit = 1000;
+ for (var i = 0; i < num.length; i++) {
+ var digit = gRomanDigits[num.charAt(i)];
+ if (last_digit < digit) {
+ Arabic -= 2 * last_digit;
+ }
+
+ last_digit = digit;
+ Arabic += last_digit;
+ }
+ return Arabic;
+ }
+
+ return "";
+}
+
+function ConvertArabicToRoman(num) {
+ if (/^\d{1,4}$/.test(num)) {
+ var digits = ("000" + num).substr(-4);
+ return (
+ gThousandsArray[digits.charAt(0)] +
+ gHundredsArray[digits.charAt(1)] +
+ gTensArray[digits.charAt(2)] +
+ gOnesArray[digits.charAt(3)]
+ );
+ }
+ return "";
+}
+
+function ConvertLettersToArabic(letters) {
+ letters = letters.toUpperCase();
+ if (!letters || /[^A-Z]/.test(letters)) {
+ return "";
+ }
+
+ var num = 0;
+ for (var i = 0; i < letters.length; i++) {
+ num = num * 26 + letters.charCodeAt(i) - A + 1;
+ }
+ return num;
+}
+
+function ConvertArabicToLetters(num) {
+ var letters = "";
+ while (num) {
+ num--;
+ letters = String.fromCharCode(A + (num % 26)) + letters;
+ num = Math.floor(num / 26);
+ }
+ return letters;
+}
+
+function onAccept(event) {
+ if (ValidateData()) {
+ // Coalesce into one undo transaction
+ var editor = GetCurrentEditor();
+
+ editor.beginTransaction();
+
+ var changeEntireList =
+ gDialog.RadioGroup.selectedItem == gDialog.ChangeAllRadio;
+
+ // Remember which radio button was selected
+ if (gListElement) {
+ gDialog.RadioGroup.setAttribute("index", changeEntireList ? "0" : "1");
+ }
+
+ var changeList;
+ if (gListElement && gDialog.ChangeAllRadio.selected) {
+ changeList = true;
+ } else {
+ changeList =
+ gMixedListSelection ||
+ gListType != gOriginalListType ||
+ gBulletStyleType != gOriginalStyleType;
+ }
+ if (changeList) {
+ try {
+ if (gListType) {
+ editor.makeOrChangeList(
+ gListType,
+ changeEntireList,
+ gBulletStyleType != gOriginalStyleType ? gBulletStyleType : null
+ );
+
+ // Get the new list created:
+ gListElement = editor.getElementOrParentByTagName(gListType, null);
+
+ editor.cloneAttributes(gListElement, globalElement);
+ } else {
+ // Remove all existing lists
+ if (gListElement && changeEntireList) {
+ editor.selectElement(gListElement);
+ }
+
+ editor.removeList("ol");
+ editor.removeList("ul");
+ editor.removeList("dl");
+ }
+ } catch (e) {}
+ }
+
+ editor.endTransaction();
+
+ SaveWindowLocation();
+
+ return;
+ }
+ event.preventDefault();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdListProps.xhtml b/comm/mail/components/compose/content/dialogs/EdListProps.xhtml
new file mode 100644
index 0000000000..b8d7c40cb2
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdListProps.xhtml
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % edListProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorListProperties.dtd">
+%edListProperties;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdListProps.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <html:fieldset>
+ <html:legend>&ListType.label;</html:legend>
+ <menulist id="ListType" oncommand="SelectListType()">
+ <menupopup>
+ <menuitem label="&none.value;" />
+ <menuitem value="ul" label="&bulletList.value;" />
+ <menuitem value="ol" label="&numberList.value;" />
+ <menuitem value="dl" label="&definitionList.value;" />
+ </menupopup>
+ </menulist>
+ </html:fieldset>
+ <spacer class="spacer" />
+
+ <!-- message text and list items are set in JS
+ text value should be identical to string with id=BulletStyle in editor.properties
+ -->
+ <html:fieldset>
+ <html:legend id="BulletStyleLabel">&bulletStyle.label;</html:legend>
+ <menulist id="BulletStyle" oncommand="SelectBulletStyle()">
+ <menupopup />
+ </menulist>
+ <spacer class="spacer" />
+ <hbox align="center">
+ <label
+ id="StartingNumberLabel"
+ control="StartingNumber"
+ value="&startingNumber.label;"
+ accesskey="&startingNumber.accessKey;"
+ />
+ <html:input
+ id="StartingNumber"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="StartingNumberLabel"
+ />
+ <spacer />
+ </hbox>
+ </html:fieldset>
+ <radiogroup id="RadioGroup" index="0" persist="index">
+ <radio
+ id="ChangeAll"
+ label="&changeEntireListRadio.label;"
+ accesskey="&changeEntireListRadio.accessKey;"
+ />
+ <radio
+ id="ChangeSelected"
+ label="&changeSelectedRadio.label;"
+ accesskey="&changeSelectedRadio.accessKey;"
+ />
+ </radiogroup>
+ <vbox id="AdvancedEdit">
+ <hbox flex="1" style="margin-top: 0.2em" align="center">
+ <!-- This will right-align the button -->
+ <spacer flex="1" />
+ <button
+ id="AdvancedEditButton1"
+ oncommand="onAdvancedEdit()"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <separator id="advancedSeparator" class="groove" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js
new file mode 100644
index 0000000000..c943cc2833
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js
@@ -0,0 +1,159 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var gInsertNew = true;
+var gAnchorElement = null;
+var gOriginalName = "";
+const kTagName = "anchor";
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+
+ gDialog.OkButton = document.querySelector("dialog").getButton("accept");
+ gDialog.NameInput = document.getElementById("nameInput");
+
+ // Get a single selected element of the desired type
+ gAnchorElement = editor.getSelectedElement(kTagName);
+
+ if (gAnchorElement) {
+ // We found an element and don't need to insert one
+ gInsertNew = false;
+
+ // Make a copy to use for AdvancedEdit
+ globalElement = gAnchorElement.cloneNode(false);
+ gOriginalName = ConvertToCDATAString(gAnchorElement.name);
+ } else {
+ gInsertNew = true;
+ // We don't have an element selected,
+ // so create one with default attributes
+ gAnchorElement = editor.createElementWithDefaults(kTagName);
+ if (gAnchorElement) {
+ // Use the current selection as suggested name
+ var name = GetSelectionAsText();
+ // Get 40 characters of the selected text and don't add "...",
+ // replace whitespace with "_" and strip non-word characters
+ name = ConvertToCDATAString(TruncateStringAtWordEnd(name, 40, false));
+ // Be sure the name is unique to the document
+ if (AnchorNameExists(name)) {
+ name += "_";
+ }
+
+ // Make a copy to use for AdvancedEdit
+ globalElement = gAnchorElement.cloneNode(false);
+ globalElement.setAttribute("name", name);
+ }
+ }
+ if (!gAnchorElement) {
+ dump("Failed to get selected element or create a new one!\n");
+ window.close();
+ return;
+ }
+
+ InitDialog();
+
+ DoEnabling();
+ SetTextboxFocus(gDialog.NameInput);
+ SetWindowLocation();
+}
+
+function InitDialog() {
+ gDialog.NameInput.value = globalElement.getAttribute("name");
+}
+
+function ChangeName() {
+ if (gDialog.NameInput.value.length > 0) {
+ // Replace spaces with "_" and strip other non-URL characters
+ // Note: we could use ConvertAndEscape, but then we'd
+ // have to UnEscapeAndConvert beforehand - too messy!
+ gDialog.NameInput.value = ConvertToCDATAString(gDialog.NameInput.value);
+ }
+ DoEnabling();
+}
+
+function DoEnabling() {
+ var enable = gDialog.NameInput.value.length > 0;
+ SetElementEnabled(gDialog.OkButton, enable);
+ SetElementEnabledById("AdvancedEditButton1", enable);
+}
+
+function AnchorNameExists(name) {
+ var anchorList;
+ try {
+ anchorList = GetCurrentEditor().document.anchors;
+ } catch (e) {}
+
+ if (anchorList) {
+ for (var i = 0; i < anchorList.length; i++) {
+ if (anchorList[i].name == name) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+// Get and validate data from widgets.
+// Set attributes on globalElement so they can be accessed by AdvancedEdit()
+function ValidateData() {
+ var name = TrimString(gDialog.NameInput.value);
+ if (!name) {
+ ShowInputErrorMessage(GetString("MissingAnchorNameError"));
+ SetTextboxFocus(gDialog.NameInput);
+ return false;
+ }
+ // Replace spaces with "_" and strip other characters
+ // Note: we could use ConvertAndEscape, but then we'd
+ // have to UnConverAndEscape beforehand - too messy!
+ name = ConvertToCDATAString(name);
+
+ if (gOriginalName != name && AnchorNameExists(name)) {
+ ShowInputErrorMessage(
+ GetString("DuplicateAnchorNameError").replace(/%name%/, name)
+ );
+ SetTextboxFocus(gDialog.NameInput);
+ return false;
+ }
+ globalElement.name = name;
+
+ return true;
+}
+
+function onAccept(event) {
+ if (ValidateData()) {
+ if (gOriginalName != globalElement.name) {
+ var editor = GetCurrentEditor();
+ editor.beginTransaction();
+
+ try {
+ // "false" = don't delete selected text when inserting
+ if (gInsertNew) {
+ // We must insert element before copying CSS style attribute,
+ // but we must set the name else it won't insert at all
+ gAnchorElement.name = globalElement.name;
+ editor.insertElementAtSelection(gAnchorElement, false);
+ }
+
+ // Copy attributes to element we are changing or inserting
+ editor.cloneAttributes(gAnchorElement, globalElement);
+ } catch (e) {}
+
+ editor.endTransaction();
+ }
+ SaveWindowLocation();
+ return;
+ }
+ event.preventDefault();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml
new file mode 100644
index 0000000000..d26f4d73b4
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % edNamedAnchorProperties SYSTEM "chrome://messenger/locale/messengercompose/EdNamedAnchorProperties.dtd">
+%edNamedAnchorProperties;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<window
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdNamedAnchorProps.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <label
+ id="nameLabel"
+ control="nameInput"
+ value="&anchorNameEditField.label;"
+ accesskey="&anchorNameEditField.accessKey;"
+ />
+ <html:input
+ id="nameInput"
+ type="text"
+ class="MinWidth20em input-inline"
+ oninput="ChangeName()"
+ title="&nameInput.tooltip;"
+ aria-labelledby="nameLabel"
+ />
+ <spacer class="spacer" />
+ <vbox id="AdvancedEdit">
+ <hbox flex="1" style="margin-top: 0.2em" align="center">
+ <!-- This will right-align the button -->
+ <spacer flex="1" />
+ <button
+ id="AdvancedEditButton1"
+ oncommand="onAdvancedEdit()"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <separator id="advancedSeparator" class="groove" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdReplace.js b/comm/mail/components/compose/content/dialogs/EdReplace.js
new file mode 100644
index 0000000000..c937702416
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdReplace.js
@@ -0,0 +1,380 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var gReplaceDialog; // Quick access to document/form elements.
+var gFindInst; // nsIWebBrowserFind that we're going to use
+var gFindService; // Global service which remembers find params
+var gEditor; // the editor we're using
+
+document.addEventListener("dialogaccept", event => {
+ onFindNext();
+ event.preventDefault();
+});
+
+function initDialogObject() {
+ // Create gReplaceDialog object and initialize.
+ gReplaceDialog = {};
+ gReplaceDialog.findInput = document.getElementById("dialog.findInput");
+ gReplaceDialog.replaceInput = document.getElementById("dialog.replaceInput");
+ gReplaceDialog.caseSensitive = document.getElementById(
+ "dialog.caseSensitive"
+ );
+ gReplaceDialog.wrap = document.getElementById("dialog.wrap");
+ gReplaceDialog.searchBackwards = document.getElementById(
+ "dialog.searchBackwards"
+ );
+ gReplaceDialog.findNext = document.getElementById("findNext");
+ gReplaceDialog.replace = document.getElementById("replace");
+ gReplaceDialog.replaceAndFind = document.getElementById("replaceAndFind");
+ gReplaceDialog.replaceAll = document.getElementById("replaceAll");
+}
+
+function loadDialog() {
+ // Set initial dialog field contents.
+ // Set initial dialog field contents. Use the gFindInst attributes first,
+ // this is necessary for window.find()
+ gReplaceDialog.findInput.value = gFindInst.searchString
+ ? gFindInst.searchString
+ : gFindService.searchString;
+ gReplaceDialog.replaceInput.value = gFindService.replaceString;
+ gReplaceDialog.caseSensitive.checked = gFindInst.matchCase
+ ? gFindInst.matchCase
+ : gFindService.matchCase;
+ gReplaceDialog.wrap.checked = gFindInst.wrapFind
+ ? gFindInst.wrapFind
+ : gFindService.wrapFind;
+ gReplaceDialog.searchBackwards.checked = gFindInst.findBackwards
+ ? gFindInst.findBackwards
+ : gFindService.findBackwards;
+
+ doEnabling();
+}
+
+function onLoad() {
+ // Get the xul <editor> element:
+ var editorElement = window.arguments[0];
+
+ // If we don't get the editor, then we won't allow replacing.
+ gEditor = editorElement.getEditor(editorElement.contentWindow);
+ if (!gEditor) {
+ window.close();
+ return;
+ }
+
+ // Get the nsIWebBrowserFind service:
+ gFindInst = editorElement.webBrowserFind;
+
+ try {
+ // get the find service, which stores global find state
+ gFindService = Cc["@mozilla.org/find/find_service;1"].getService(
+ Ci.nsIFindService
+ );
+ } catch (e) {
+ dump("No find service!\n");
+ gFindService = 0;
+ }
+
+ // Init gReplaceDialog.
+ initDialogObject();
+
+ // Change "OK" to "Find".
+ // dialog.find.label = document.getElementById("fBLT").getAttribute("label");
+
+ // Fill dialog.
+ loadDialog();
+
+ if (gReplaceDialog.findInput.value) {
+ gReplaceDialog.findInput.select();
+ } else {
+ gReplaceDialog.findInput.focus();
+ }
+}
+
+function saveFindData() {
+ // Set data attributes per user input.
+ if (gFindService) {
+ gFindService.searchString = gReplaceDialog.findInput.value;
+ gFindService.matchCase = gReplaceDialog.caseSensitive.checked;
+ gFindService.wrapFind = gReplaceDialog.wrap.checked;
+ gFindService.findBackwards = gReplaceDialog.searchBackwards.checked;
+ }
+}
+
+function setUpFindInst() {
+ gFindInst.searchString = gReplaceDialog.findInput.value;
+ gFindInst.matchCase = gReplaceDialog.caseSensitive.checked;
+ gFindInst.wrapFind = gReplaceDialog.wrap.checked;
+ gFindInst.findBackwards = gReplaceDialog.searchBackwards.checked;
+}
+
+function onFindNext() {
+ // Transfer dialog contents to the find service.
+ saveFindData();
+ // set up the find instance
+ setUpFindInst();
+
+ // Search.
+ var result = gFindInst.findNext();
+
+ if (!result) {
+ var bundle = document.getElementById("findBundle");
+ Services.prompt.alert(
+ window,
+ GetString("Alert"),
+ bundle.getString("notFoundWarning")
+ );
+ SetTextboxFocus(gReplaceDialog.findInput);
+ gReplaceDialog.findInput.select();
+ gReplaceDialog.findInput.focus();
+ return false;
+ }
+ return true;
+}
+
+function onReplace() {
+ if (!gEditor) {
+ return false;
+ }
+
+ // Does the current selection match the find string?
+ var selection = gEditor.selection;
+
+ var selStr = selection.toString();
+ var specStr = gReplaceDialog.findInput.value;
+ if (!gReplaceDialog.caseSensitive.checked) {
+ selStr = selStr.toLowerCase();
+ specStr = specStr.toLowerCase();
+ }
+ // Unfortunately, because of whitespace we can't just check
+ // whether (selStr == specStr), but have to loop ourselves.
+ // N chars of whitespace in specStr can match any M >= N in selStr.
+ var matches = true;
+ var specLen = specStr.length;
+ var selLen = selStr.length;
+ if (selLen < specLen) {
+ matches = false;
+ } else {
+ var specArray = specStr.match(/\S+|\s+/g);
+ var selArray = selStr.match(/\S+|\s+/g);
+ if (specArray.length != selArray.length) {
+ matches = false;
+ } else {
+ for (var i = 0; i < selArray.length; i++) {
+ if (selArray[i] != specArray[i]) {
+ if (/\S/.test(selArray[i][0]) || /\S/.test(specArray[i][0])) {
+ // not a space chunk -- match fails
+ matches = false;
+ break;
+ } else if (selArray[i].length < specArray[i].length) {
+ // if it's a space chunk then we only care that sel be
+ // at least as long as spec
+ matches = false;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // If the current selection doesn't match the pattern,
+ // then we want to find the next match, but not do the replace.
+ // That's what most other apps seem to do.
+ // So here, just return.
+ if (!matches) {
+ return false;
+ }
+
+ // Transfer dialog contents to the find service.
+ saveFindData();
+
+ // For reverse finds, need to remember the caret position
+ // before current selection
+ var newRange;
+ if (gReplaceDialog.searchBackwards.checked && selection.rangeCount > 0) {
+ newRange = selection.getRangeAt(0).cloneRange();
+ newRange.collapse(true);
+ }
+
+ // nsPlaintextEditor::InsertText fails if the string is empty,
+ // so make that a special case:
+ var replStr = gReplaceDialog.replaceInput.value;
+ if (replStr == "") {
+ gEditor.deleteSelection(gEditor.eNone, gEditor.eStrip);
+ } else {
+ gEditor.insertText(replStr);
+ }
+
+ // For reverse finds, need to move caret just before the replaced text
+ if (gReplaceDialog.searchBackwards.checked && newRange) {
+ gEditor.selection.removeAllRanges();
+ gEditor.selection.addRange(newRange);
+ }
+
+ return true;
+}
+
+function onReplaceAll() {
+ if (!gEditor) {
+ return;
+ }
+
+ var findStr = gReplaceDialog.findInput.value;
+ var repStr = gReplaceDialog.replaceInput.value;
+
+ // Transfer dialog contents to the find service.
+ saveFindData();
+
+ var finder = Cc["@mozilla.org/embedcomp/rangefind;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsIFind);
+
+ finder.caseSensitive = gReplaceDialog.caseSensitive.checked;
+ finder.findBackwards = gReplaceDialog.searchBackwards.checked;
+
+ // We want the whole operation to be undoable in one swell foop,
+ // so start a transaction:
+ gEditor.beginTransaction();
+
+ // and to make sure we close the transaction, guard against exceptions:
+ try {
+ // Make a range containing the current selection,
+ // so we don't go past it when we wrap.
+ var selection = gEditor.selection;
+ var selecRange;
+ if (selection.rangeCount > 0) {
+ selecRange = selection.getRangeAt(0);
+ }
+ var origRange = selecRange.cloneRange();
+
+ // We'll need a range for the whole document:
+ var wholeDocRange = gEditor.document.createRange();
+ var rootNode = gEditor.rootElement;
+ wholeDocRange.selectNodeContents(rootNode);
+
+ // And start and end points:
+ var endPt = gEditor.document.createRange();
+
+ if (gReplaceDialog.searchBackwards.checked) {
+ endPt.setStart(wholeDocRange.startContainer, wholeDocRange.startOffset);
+ endPt.setEnd(wholeDocRange.startContainer, wholeDocRange.startOffset);
+ } else {
+ endPt.setStart(wholeDocRange.endContainer, wholeDocRange.endOffset);
+ endPt.setEnd(wholeDocRange.endContainer, wholeDocRange.endOffset);
+ }
+
+ // Find and replace from here to end (start) of document:
+ var foundRange;
+ var searchRange = wholeDocRange.cloneRange();
+ while (
+ (foundRange = finder.Find(findStr, searchRange, selecRange, endPt)) !=
+ null
+ ) {
+ gEditor.selection.removeAllRanges();
+ gEditor.selection.addRange(foundRange);
+
+ // The editor will leave the caret at the end of the replaced text.
+ // For reverse finds, we need it at the beginning,
+ // so save the next position now.
+ if (gReplaceDialog.searchBackwards.checked) {
+ selecRange = foundRange.cloneRange();
+ selecRange.setEnd(selecRange.startContainer, selecRange.startOffset);
+ }
+
+ // nsPlaintextEditor::InsertText fails if the string is empty,
+ // so make that a special case:
+ if (repStr == "") {
+ gEditor.deleteSelection(gEditor.eNone, gEditor.eStrip);
+ } else {
+ gEditor.insertText(repStr);
+ }
+
+ // If we're going forward, we didn't save selecRange before, so do it now:
+ if (!gReplaceDialog.searchBackwards.checked) {
+ selection = gEditor.selection;
+ if (selection.rangeCount <= 0) {
+ gEditor.endTransaction();
+ return;
+ }
+ selecRange = selection.getRangeAt(0).cloneRange();
+ }
+ }
+
+ // If no wrapping, then we're done
+ if (!gReplaceDialog.wrap.checked) {
+ gEditor.endTransaction();
+ return;
+ }
+
+ // If wrapping, find from start/end of document back to start point.
+ if (gReplaceDialog.searchBackwards.checked) {
+ // Collapse origRange to end
+ origRange.setStart(origRange.endContainer, origRange.endOffset);
+ // Set current position to document end
+ selecRange.setEnd(wholeDocRange.endContainer, wholeDocRange.endOffset);
+ selecRange.setStart(wholeDocRange.endContainer, wholeDocRange.endOffset);
+ } else {
+ // Collapse origRange to start
+ origRange.setEnd(origRange.startContainer, origRange.startOffset);
+ // Set current position to document start
+ selecRange.setStart(
+ wholeDocRange.startContainer,
+ wholeDocRange.startOffset
+ );
+ selecRange.setEnd(
+ wholeDocRange.startContainer,
+ wholeDocRange.startOffset
+ );
+ }
+
+ while (
+ (foundRange = finder.Find(
+ findStr,
+ wholeDocRange,
+ selecRange,
+ origRange
+ )) != null
+ ) {
+ gEditor.selection.removeAllRanges();
+ gEditor.selection.addRange(foundRange);
+
+ // Save insert point for backward case
+ if (gReplaceDialog.searchBackwards.checked) {
+ selecRange = foundRange.cloneRange();
+ selecRange.setEnd(selecRange.startContainer, selecRange.startOffset);
+ }
+
+ // nsPlaintextEditor::InsertText fails if the string is empty,
+ // so make that a special case:
+ if (repStr == "") {
+ gEditor.deleteSelection(gEditor.eNone, gEditor.eStrip);
+ } else {
+ gEditor.insertText(repStr);
+ }
+
+ // Get insert point for forward case
+ if (!gReplaceDialog.searchBackwards.checked) {
+ selection = gEditor.selection;
+ if (selection.rangeCount <= 0) {
+ gEditor.endTransaction();
+ return;
+ }
+ selecRange = selection.getRangeAt(0);
+ }
+ }
+ } catch (e) {}
+
+ gEditor.endTransaction();
+}
+
+function doEnabling() {
+ var findStr = gReplaceDialog.findInput.value;
+ gReplaceDialog.enabled = findStr;
+ gReplaceDialog.findNext.disabled = !findStr;
+ gReplaceDialog.replace.disabled = !findStr;
+ gReplaceDialog.replaceAndFind.disabled = !findStr;
+ gReplaceDialog.replaceAll.disabled = !findStr;
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdReplace.xhtml b/comm/mail/components/compose/content/dialogs/EdReplace.xhtml
new file mode 100644
index 0000000000..62ce5a67e2
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdReplace.xhtml
@@ -0,0 +1,126 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorReplace.dtd">
+
+<window
+ id="replaceDlg"
+ title="&replaceDialog.title;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="screenX screenY"
+ lightweightthemes="true"
+ onload="onLoad()"
+>
+ <dialog buttons="cancel">
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdReplace.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <stringbundle
+ id="findBundle"
+ src="chrome://global/locale/finddialog.properties"
+ />
+
+ <hbox>
+ <vbox>
+ <spacer class="spacer" />
+ <html:div class="grid-two-column">
+ <html:div class="flex-items-center">
+ <label
+ value="&findField.label;"
+ accesskey="&findField.accesskey;"
+ control="dialog.findInput"
+ />
+ </html:div>
+ <html:div>
+ <html:input
+ id="dialog.findInput"
+ class="input-inline"
+ oninput="doEnabling();"
+ />
+ </html:div>
+ <html:div class="flex-items-center">
+ <label
+ value="&replaceField.label;"
+ accesskey="&replaceField.accesskey;"
+ control="dialog.replaceInput"
+ />
+ </html:div>
+ <html:div>
+ <html:input
+ id="dialog.replaceInput"
+ class="input-inline"
+ oninput="doEnabling();"
+ />
+ </html:div>
+ <html:div class="grid-item-col2">
+ <vbox align="start">
+ <checkbox
+ id="dialog.caseSensitive"
+ label="&caseSensitiveCheckbox.label;"
+ accesskey="&caseSensitiveCheckbox.accesskey;"
+ />
+ <checkbox
+ id="dialog.wrap"
+ label="&wrapCheckbox.label;"
+ accesskey="&wrapCheckbox.accesskey;"
+ />
+ <checkbox
+ id="dialog.searchBackwards"
+ label="&backwardsCheckbox.label;"
+ accesskey="&backwardsCheckbox.accesskey;"
+ />
+ </vbox>
+ </html:div>
+ </html:div>
+ </vbox>
+ <spacer class="spacer" />
+ <vbox>
+ <button
+ id="findNext"
+ label="&findNextButton.label;"
+ accesskey="&findNextButton.accesskey;"
+ oncommand="onFindNext();"
+ default="true"
+ />
+ <button
+ id="replace"
+ label="&replaceButton.label;"
+ accesskey="&replaceButton.accesskey;"
+ oncommand="onReplace();"
+ />
+ <button
+ id="replaceAndFind"
+ label="&replaceAndFindButton.label;"
+ accesskey="&replaceAndFindButton.accesskey;"
+ oncommand="onReplace(); onFindNext();"
+ />
+ <button
+ id="replaceAll"
+ label="&replaceAllButton.label;"
+ accesskey="&replaceAllButton.accesskey;"
+ oncommand="onReplaceAll();"
+ />
+ <button
+ dlgtype="cancel"
+ label="&closeButton.label;"
+ accesskey="&closeButton.accesskey;"
+ />
+ </vbox>
+ </hbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdSpellCheck.js b/comm/mail/components/compose/content/dialogs/EdSpellCheck.js
new file mode 100644
index 0000000000..5b54205bc3
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdSpellCheck.js
@@ -0,0 +1,496 @@
+/* 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 ../../../../base/content/utilityOverlay.js */
+/* import-globals-from ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+var { InlineSpellChecker } = ChromeUtils.importESModule(
+ "resource://gre/modules/InlineSpellChecker.sys.mjs"
+);
+
+var gMisspelledWord;
+var gSpellChecker = null;
+var gAllowSelectWord = true;
+var gPreviousReplaceWord = "";
+var gFirstTime = true;
+var gDictCount = 0;
+
+document.addEventListener("dialogaccept", doDefault);
+document.addEventListener("dialogcancel", CancelSpellCheck);
+
+function Startup() {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ window.close();
+ return;
+ }
+
+ // Get the spellChecker shell
+ gSpellChecker = Cu.createSpellChecker();
+ if (!gSpellChecker) {
+ dump("SpellChecker not found!!!\n");
+ window.close();
+ return;
+ }
+
+ // Start the spell checker module.
+ try {
+ var skipBlockQuotes = window.arguments[1];
+ var enableSelectionChecking = window.arguments[2];
+
+ gSpellChecker.setFilterType(
+ skipBlockQuotes
+ ? Ci.nsIEditorSpellCheck.FILTERTYPE_MAIL
+ : Ci.nsIEditorSpellCheck.FILTERTYPE_NORMAL
+ );
+ gSpellChecker.InitSpellChecker(
+ editor,
+ enableSelectionChecking,
+ spellCheckStarted
+ );
+ } catch (ex) {
+ dump("*** Exception error: InitSpellChecker\n");
+ window.close();
+ }
+}
+
+function spellCheckStarted() {
+ gDialog.MisspelledWordLabel = document.getElementById("MisspelledWordLabel");
+ gDialog.MisspelledWord = document.getElementById("MisspelledWord");
+ gDialog.ReplaceButton = document.getElementById("Replace");
+ gDialog.IgnoreButton = document.getElementById("Ignore");
+ gDialog.StopButton = document.getElementById("Stop");
+ gDialog.CloseButton = document.getElementById("Close");
+ gDialog.ReplaceWordInput = document.getElementById("ReplaceWordInput");
+ gDialog.SuggestedList = document.getElementById("SuggestedList");
+ gDialog.LanguageMenulist = document.getElementById("LanguageMenulist");
+
+ // Fill in the language menulist and sync it up
+ // with the spellchecker's current language.
+
+ var curLangs;
+
+ try {
+ curLangs = new Set(gSpellChecker.getCurrentDictionaries());
+ } catch (ex) {
+ curLangs = new Set();
+ }
+
+ InitLanguageMenu(curLangs);
+
+ // Get the first misspelled word and setup all UI
+ NextWord();
+
+ // When startup param is true, setup different UI when spell checking
+ // just before sending mail message
+ if (window.arguments[0]) {
+ // If no misspelled words found, simply close dialog and send message
+ if (!gMisspelledWord) {
+ onClose();
+ return;
+ }
+
+ // Hide "Close" button and use "Send" instead
+ gDialog.CloseButton.hidden = true;
+ gDialog.CloseButton = document.getElementById("Send");
+ gDialog.CloseButton.hidden = false;
+ } else {
+ // Normal spell checking - hide the "Stop" button
+ // (Note that this button is the "Cancel" button for
+ // Esc keybinding and related window close actions)
+ gDialog.StopButton.hidden = true;
+ }
+
+ // Clear flag that determines message when
+ // no misspelled word is found
+ // (different message when used for the first time)
+ gFirstTime = false;
+
+ window.sizeToContent();
+}
+
+/**
+ * Populate the dictionary language selector menu.
+ *
+ * @param {Set<string>} activeDictionaries - Currently active dictionaries.
+ */
+function InitLanguageMenu(activeDictionaries) {
+ // Get the list of dictionaries from
+ // the spellchecker.
+
+ var dictList;
+ try {
+ dictList = gSpellChecker.GetDictionaryList();
+ } catch (ex) {
+ dump("Failed to get DictionaryList!\n");
+ return;
+ }
+
+ // If we're not just starting up and dictionary count
+ // hasn't changed then no need to update the menu.
+ if (gDictCount == dictList.length) {
+ return;
+ }
+
+ // Store current dictionary count.
+ gDictCount = dictList.length;
+
+ var inlineSpellChecker = new InlineSpellChecker();
+ var sortedList = inlineSpellChecker.sortDictionaryList(dictList);
+
+ // Remove any languages from the list.
+ let list = document.getElementById("dictionary-list");
+ let template = document.getElementById("language-item");
+
+ list.replaceChildren(
+ ...sortedList.map(({ displayName, localeCode }) => {
+ let item = template.content.cloneNode(true);
+ item.querySelector(".checkbox-label").textContent = displayName;
+ let input = item.querySelector("input");
+ input.addEventListener("input", () => {
+ SelectLanguage(localeCode);
+ });
+ input.checked = activeDictionaries.has(localeCode);
+ return item;
+ })
+ );
+}
+
+function DoEnabling() {
+ if (!gMisspelledWord) {
+ // No more misspelled words
+ gDialog.MisspelledWord.setAttribute(
+ "value",
+ GetString(gFirstTime ? "NoMisspelledWord" : "CheckSpellingDone")
+ );
+
+ gDialog.ReplaceButton.removeAttribute("default");
+ gDialog.IgnoreButton.removeAttribute("default");
+
+ gDialog.CloseButton.setAttribute("default", "true");
+ // Shouldn't have to do this if "default" is true?
+ gDialog.CloseButton.focus();
+
+ SetElementEnabledById("MisspelledWordLabel", false);
+ SetElementEnabledById("ReplaceWordLabel", false);
+ SetElementEnabledById("ReplaceWordInput", false);
+ SetElementEnabledById("CheckWord", false);
+ SetElementEnabledById("SuggestedListLabel", false);
+ SetElementEnabledById("SuggestedList", false);
+ SetElementEnabledById("Ignore", false);
+ SetElementEnabledById("IgnoreAll", false);
+ SetElementEnabledById("Replace", false);
+ SetElementEnabledById("ReplaceAll", false);
+ SetElementEnabledById("AddToDictionary", false);
+ } else {
+ SetElementEnabledById("MisspelledWordLabel", true);
+ SetElementEnabledById("ReplaceWordLabel", true);
+ SetElementEnabledById("ReplaceWordInput", true);
+ SetElementEnabledById("CheckWord", true);
+ SetElementEnabledById("SuggestedListLabel", true);
+ SetElementEnabledById("SuggestedList", true);
+ SetElementEnabledById("Ignore", true);
+ SetElementEnabledById("IgnoreAll", true);
+ SetElementEnabledById("AddToDictionary", true);
+
+ gDialog.CloseButton.removeAttribute("default");
+ SetReplaceEnable();
+ }
+}
+
+function NextWord() {
+ gMisspelledWord = gSpellChecker.GetNextMisspelledWord();
+ SetWidgetsForMisspelledWord();
+}
+
+function SetWidgetsForMisspelledWord() {
+ gDialog.MisspelledWord.setAttribute("value", gMisspelledWord);
+
+ // Initial replace word is misspelled word
+ gDialog.ReplaceWordInput.value = gMisspelledWord;
+ gPreviousReplaceWord = gMisspelledWord;
+
+ // This sets gDialog.ReplaceWordInput to first suggested word in list
+ FillSuggestedList(gMisspelledWord);
+
+ DoEnabling();
+
+ if (gMisspelledWord) {
+ SetTextboxFocus(gDialog.ReplaceWordInput);
+ }
+}
+
+function CheckWord() {
+ var word = gDialog.ReplaceWordInput.value;
+ if (word) {
+ if (gSpellChecker.CheckCurrentWord(word)) {
+ FillSuggestedList(word);
+ SetReplaceEnable();
+ } else {
+ ClearListbox(gDialog.SuggestedList);
+ var item = gDialog.SuggestedList.appendItem(
+ GetString("CorrectSpelling"),
+ ""
+ );
+ if (item) {
+ item.setAttribute("disabled", "true");
+ }
+ // Suppress being able to select the message text
+ gAllowSelectWord = false;
+ }
+ }
+}
+
+function SelectSuggestedWord() {
+ if (gAllowSelectWord) {
+ if (gDialog.SuggestedList.selectedItem) {
+ var selValue = gDialog.SuggestedList.selectedItem.label;
+ gDialog.ReplaceWordInput.value = selValue;
+ gPreviousReplaceWord = selValue;
+ } else {
+ gDialog.ReplaceWordInput.value = gPreviousReplaceWord;
+ }
+ SetReplaceEnable();
+ }
+}
+
+function ChangeReplaceWord() {
+ // Calling this triggers SelectSuggestedWord(),
+ // so temporarily suppress the effect of that
+ var saveAllow = gAllowSelectWord;
+ gAllowSelectWord = false;
+
+ // Select matching word in list
+ var newSelectedItem;
+ var replaceWord = TrimString(gDialog.ReplaceWordInput.value);
+ if (replaceWord) {
+ for (var i = 0; i < gDialog.SuggestedList.getRowCount(); i++) {
+ var item = gDialog.SuggestedList.getItemAtIndex(i);
+ if (item.label == replaceWord) {
+ newSelectedItem = item;
+ break;
+ }
+ }
+ }
+ gDialog.SuggestedList.selectedItem = newSelectedItem;
+
+ gAllowSelectWord = saveAllow;
+
+ // Remember the new word
+ gPreviousReplaceWord = gDialog.ReplaceWordInput.value;
+
+ SetReplaceEnable();
+}
+
+function Ignore() {
+ NextWord();
+}
+
+function IgnoreAll() {
+ if (gMisspelledWord) {
+ gSpellChecker.IgnoreWordAllOccurrences(gMisspelledWord);
+ }
+ NextWord();
+}
+
+function Replace(newWord) {
+ if (!newWord) {
+ return;
+ }
+
+ if (gMisspelledWord && gMisspelledWord != newWord) {
+ var editor = GetCurrentEditor();
+ editor.beginTransaction();
+ try {
+ gSpellChecker.ReplaceWord(gMisspelledWord, newWord, false);
+ } catch (e) {}
+ editor.endTransaction();
+ }
+ NextWord();
+}
+
+function ReplaceAll() {
+ var newWord = gDialog.ReplaceWordInput.value;
+ if (gMisspelledWord && gMisspelledWord != newWord) {
+ var editor = GetCurrentEditor();
+ editor.beginTransaction();
+ try {
+ gSpellChecker.ReplaceWord(gMisspelledWord, newWord, true);
+ } catch (e) {}
+ editor.endTransaction();
+ }
+ NextWord();
+}
+
+function AddToDictionary() {
+ if (gMisspelledWord) {
+ gSpellChecker.AddWordToDictionary(gMisspelledWord);
+ }
+ NextWord();
+}
+
+function EditDictionary() {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdDictionary.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ "",
+ gMisspelledWord
+ );
+}
+
+/**
+ * Change the selection state of the given dictionary language.
+ *
+ * @param {string} language
+ */
+function SelectLanguage(language) {
+ let activeDictionaries = new Set(gSpellChecker.getCurrentDictionaries());
+ if (activeDictionaries.has(language)) {
+ activeDictionaries.delete(language);
+ } else {
+ activeDictionaries.add(language);
+ }
+ let activeDictionariesArray = Array.from(activeDictionaries);
+ gSpellChecker.setCurrentDictionaries(activeDictionariesArray);
+ // For compose windows we need to set the "lang" attribute so the
+ // core editor uses the correct dictionary for the inline spell check.
+ if (window.arguments[1]) {
+ if ("ComposeChangeLanguage" in window.opener) {
+ // We came here from a compose window.
+ window.opener.ComposeChangeLanguage(activeDictionariesArray);
+ } else if (activeDictionaries.size === 1) {
+ window.opener.document.documentElement.setAttribute(
+ "lang",
+ activeDictionariesArray[0]
+ );
+ } else {
+ window.opener.document.documentElement.setAttribute("lang", "");
+ }
+ }
+}
+
+function Recheck() {
+ var recheckLanguages;
+
+ function finishRecheck() {
+ gSpellChecker.setCurrentDictionaries(recheckLanguages);
+ gMisspelledWord = gSpellChecker.GetNextMisspelledWord();
+ SetWidgetsForMisspelledWord();
+ }
+
+ // TODO: Should we bother to add a "Recheck" method to interface?
+ try {
+ recheckLanguages = gSpellChecker.getCurrentDictionaries();
+ gSpellChecker.UninitSpellChecker();
+ // Clear the ignore all list.
+ Cc["@mozilla.org/spellchecker/personaldictionary;1"]
+ .getService(Ci.mozIPersonalDictionary)
+ .endSession();
+ gSpellChecker.InitSpellChecker(GetCurrentEditor(), false, finishRecheck);
+ } catch (ex) {
+ console.error(ex);
+ }
+}
+
+function FillSuggestedList(misspelledWord) {
+ var list = gDialog.SuggestedList;
+
+ // Clear the current contents of the list
+ gAllowSelectWord = false;
+ ClearListbox(list);
+ var item;
+
+ if (misspelledWord.length > 0) {
+ // Get suggested words until an empty string is returned
+ var count = 0;
+ do {
+ var word = gSpellChecker.GetSuggestedWord();
+ if (word.length > 0) {
+ list.appendItem(word, "");
+ count++;
+ }
+ } while (word.length > 0);
+
+ if (count == 0) {
+ // No suggestions - show a message but don't let user select it
+ item = list.appendItem(GetString("NoSuggestedWords"));
+ if (item) {
+ item.setAttribute("disabled", "true");
+ }
+ gAllowSelectWord = false;
+ } else {
+ gAllowSelectWord = true;
+ // Initialize with first suggested list by selecting it
+ gDialog.SuggestedList.selectedIndex = 0;
+ }
+ } else {
+ item = list.appendItem("", "");
+ if (item) {
+ item.setAttribute("disabled", "true");
+ }
+ }
+}
+
+function SetReplaceEnable() {
+ // Enable "Change..." buttons only if new word is different than misspelled
+ var newWord = gDialog.ReplaceWordInput.value;
+ var enable = newWord.length > 0 && newWord != gMisspelledWord;
+ SetElementEnabledById("Replace", enable);
+ SetElementEnabledById("ReplaceAll", enable);
+ if (enable) {
+ gDialog.ReplaceButton.setAttribute("default", "true");
+ gDialog.IgnoreButton.removeAttribute("default");
+ } else {
+ gDialog.IgnoreButton.setAttribute("default", "true");
+ gDialog.ReplaceButton.removeAttribute("default");
+ }
+}
+
+function doDefault(event) {
+ if (gDialog.ReplaceButton.getAttribute("default") == "true") {
+ Replace(gDialog.ReplaceWordInput.value);
+ } else if (gDialog.IgnoreButton.getAttribute("default") == "true") {
+ Ignore();
+ } else if (gDialog.CloseButton.getAttribute("default") == "true") {
+ onClose();
+ }
+
+ event.preventDefault();
+}
+
+function ExitSpellChecker() {
+ if (gSpellChecker) {
+ try {
+ gSpellChecker.UninitSpellChecker();
+ // now check the document over again with the new dictionary
+ // if we have an inline spellchecker
+ if (
+ "InlineSpellCheckerUI" in window.opener &&
+ window.opener.InlineSpellCheckerUI.enabled
+ ) {
+ window.opener.InlineSpellCheckerUI.mInlineSpellChecker.spellCheckRange(
+ null
+ );
+ }
+ } finally {
+ gSpellChecker = null;
+ }
+ }
+}
+
+function CancelSpellCheck() {
+ ExitSpellChecker();
+
+ // Signal to calling window that we canceled
+ window.opener.cancelSendMessage = true;
+}
+
+function onClose() {
+ ExitSpellChecker();
+
+ window.opener.cancelSendMessage = false;
+ window.close();
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml b/comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml
new file mode 100644
index 0000000000..fcff0e1703
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml
@@ -0,0 +1,209 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorSpellCheck.dtd">
+
+<!-- dialog containing a control requiring initial setup -->
+<window
+ id="spellCheckDlg"
+ title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="screenX screenY"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog buttons="cancel">
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://communicator/content/utilityOverlay.js" />
+ <script src="chrome://messenger/content/messengercompose/EdSpellCheck.js" />
+ <script src="chrome://global/content/contentAreaUtils.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <stringbundle
+ id="languageBundle"
+ src="chrome://global/locale/languageNames.properties"
+ />
+ <stringbundle
+ id="regionBundle"
+ src="chrome://global/locale/regionNames.properties"
+ />
+
+ <html:div class="grid-three-column-auto-x-auto">
+ <html:div class="flex-items-center">
+ <label id="MisspelledWordLabel" value="&misspelledWord.label;" />
+ </html:div>
+ <html:div class="flex-items-center">
+ <label id="MisspelledWord" class="bold" crop="end" />
+ </html:div>
+ <html:div class="flex-items-center">
+ <button
+ class="spell-check"
+ label="&recheckButton2.label;"
+ oncommand="Recheck();"
+ accesskey="&recheckButton2.accessKey;"
+ />
+ </html:div>
+ <html:div class="flex-items-center">
+ <label
+ id="ReplaceWordLabel"
+ value="&wordEditField.label;"
+ control="ReplaceWordInput"
+ accesskey="&wordEditField.accessKey;"
+ />
+ </html:div>
+ <html:div>
+ <hbox flex="1" class="input-container">
+ <html:input
+ id="ReplaceWordInput"
+ type="text"
+ class="input-inline"
+ onchange="ChangeReplaceWord()"
+ aria-labelledby="ReplaceWordLabel"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="flex-items-center">
+ <button
+ id="CheckWord"
+ class="spell-check"
+ oncommand="CheckWord()"
+ label="&checkwordButton.label;"
+ accesskey="&checkwordButton.accessKey;"
+ />
+ </html:div>
+ </html:div>
+ <label
+ id="SuggestedListLabel"
+ value="&suggestions.label;"
+ control="SuggestedList"
+ accesskey="&suggestions.accessKey;"
+ />
+ <hbox flex="1" class="display-flex">
+ <html:div class="grid-two-column-x-auto flex-1">
+ <html:div class="display-flex">
+ <richlistbox
+ id="SuggestedList"
+ class="display-flex flex-1"
+ onselect="SelectSuggestedWord()"
+ ondblclick="if (gAllowSelectWord) { Replace(event.target.value); }"
+ />
+ </html:div>
+ <html:div>
+ <vbox>
+ <html:div class="grid-two-column-equalsize">
+ <button
+ id="Replace"
+ class="spell-check"
+ label="&replaceButton.label;"
+ oncommand="Replace(gDialog.ReplaceWordInput.value);"
+ accesskey="&replaceButton.accessKey;"
+ />
+ <button
+ id="Ignore"
+ class="spell-check"
+ oncommand="Ignore();"
+ label="&ignoreButton.label;"
+ accesskey="&ignoreButton.accessKey;"
+ />
+ <button
+ id="ReplaceAll"
+ class="spell-check"
+ oncommand="ReplaceAll();"
+ label="&replaceAllButton.label;"
+ accesskey="&replaceAllButton.accessKey;"
+ />
+ <button
+ id="IgnoreAll"
+ class="spell-check"
+ oncommand="IgnoreAll();"
+ label="&ignoreAllButton.label;"
+ accesskey="&ignoreAllButton.accessKey;"
+ />
+ </html:div>
+ <separator />
+ <label value="&userDictionary.label;" />
+ <hbox align="start">
+ <button
+ id="AddToDictionary"
+ class="spell-check"
+ oncommand="AddToDictionary()"
+ label="&addToUserDictionaryButton.label;"
+ accesskey="&addToUserDictionaryButton.accessKey;"
+ />
+ <button
+ id="EditDictionary"
+ class="spell-check"
+ oncommand="EditDictionary()"
+ label="&editUserDictionaryButton.label;"
+ accesskey="&editUserDictionaryButton.accessKey;"
+ />
+ </hbox>
+ </vbox>
+ </html:div>
+ <html:div class="grid-item-span-row">
+ <label
+ value="&languagePopup.label;"
+ control="LanguageMenulist"
+ accesskey="&languagePopup.accessKey;"
+ />
+ </html:div>
+ <html:div>
+ <html:ul id="dictionary-list"> </html:ul>
+ <html:template id="language-item"
+ ><html:li>
+ <html:label
+ ><html:input type="checkbox"></html:input>
+ <html:span class="checkbox-label"></html:span
+ ></html:label> </html:li
+ ></html:template>
+ <html:a onclick="openDictionaryList()" href=""
+ >&moreDictionaries.label;</html:a
+ >
+ </html:div>
+ <html:div>
+ <hbox class="display-flex">
+ <button
+ id="Stop"
+ class="spell-check"
+ dlgtype="cancel"
+ label="&stopButton.label;"
+ oncommand="CancelSpellCheck();"
+ accesskey="&stopButton.accessKey;"
+ />
+ <spacer class="flex-1" />
+ <button
+ id="Close"
+ class="spell-check"
+ label="&closeButton.label;"
+ oncommand="onClose();"
+ accesskey="&closeButton.accessKey;"
+ />
+ <button
+ id="Send"
+ class="spell-check"
+ label="&sendButton.label;"
+ oncommand="onClose();"
+ accesskey="&sendButton.accessKey;"
+ hidden="true"
+ />
+ </hbox>
+ </html:div>
+ </html:div>
+ </hbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/dialogs/EdTableProps.js b/comm/mail/components/compose/content/dialogs/EdTableProps.js
new file mode 100644
index 0000000000..fd4ab40f3a
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdTableProps.js
@@ -0,0 +1,1426 @@
+/* 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 ../editorUtilities.js */
+/* import-globals-from EdDialogCommon.js */
+
+// Cancel() is in EdDialogCommon.js
+
+var gTableElement;
+var gCellElement;
+var gTableCaptionElement;
+var globalCellElement;
+var globalTableElement;
+var gValidateTab;
+const defHAlign = "left";
+const centerStr = "center"; // Index=1
+const rightStr = "right"; // 2
+const justifyStr = "justify"; // 3
+const charStr = "char"; // 4
+const defVAlign = "middle";
+const topStr = "top";
+const bottomStr = "bottom";
+const bgcolor = "bgcolor";
+var gTableColor;
+var gCellColor;
+
+const cssBackgroundColorStr = "background-color";
+
+var gRowCount = 1;
+var gColCount = 1;
+var gLastRowIndex;
+var gLastColIndex;
+var gNewRowCount;
+var gNewColCount;
+var gCurRowIndex;
+var gCurColIndex;
+var gCurColSpan;
+var gSelectedCellsType = 1;
+const SELECT_CELL = 1;
+const SELECT_ROW = 2;
+const SELECT_COLUMN = 3;
+const RESET_SELECTION = 0;
+var gCellData = {
+ value: null,
+ startRowIndex: 0,
+ startColIndex: 0,
+ rowSpan: 0,
+ colSpan: 0,
+ actualRowSpan: 0,
+ actualColSpan: 0,
+ isSelected: false,
+};
+var gAdvancedEditUsed;
+var gAlignWasChar = false;
+
+/*
+From C++:
+ 0 TABLESELECTION_TABLE
+ 1 TABLESELECTION_CELL There are 1 or more cells selected
+ but complete rows or columns are not selected
+ 2 TABLESELECTION_ROW All cells are in 1 or more rows
+ and in each row, all cells selected
+ Note: This is the value if all rows (thus all cells) are selected
+ 3 TABLESELECTION_COLUMN All cells are in 1 or more columns
+*/
+
+var gSelectedCellCount = 0;
+var gApplyUsed = false;
+var gSelection;
+var gCellDataChanged = false;
+var gCanDelete = false;
+var gUseCSS = true;
+var gActiveEditor;
+
+// dialog initialization code
+
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogextra1", Apply);
+document.addEventListener("dialogcancel", onCancel);
+
+function Startup() {
+ gActiveEditor = GetCurrentTableEditor();
+ if (!gActiveEditor) {
+ window.close();
+ return;
+ }
+
+ try {
+ gSelection = gActiveEditor.selection;
+ } catch (e) {}
+ if (!gSelection) {
+ return;
+ }
+
+ // Get dialog widgets - Table Panel
+ gDialog.TableRowsInput = document.getElementById("TableRowsInput");
+ gDialog.TableColumnsInput = document.getElementById("TableColumnsInput");
+ gDialog.TableWidthInput = document.getElementById("TableWidthInput");
+ gDialog.TableWidthUnits = document.getElementById("TableWidthUnits");
+ gDialog.TableHeightInput = document.getElementById("TableHeightInput");
+ gDialog.TableHeightUnits = document.getElementById("TableHeightUnits");
+ try {
+ if (
+ !Services.prefs.getBoolPref("editor.use_css") ||
+ gActiveEditor.flags & 1
+ ) {
+ gUseCSS = false;
+ var tableHeightLabel = document.getElementById("TableHeightLabel");
+ tableHeightLabel.remove();
+ gDialog.TableHeightInput.remove();
+ gDialog.TableHeightUnits.remove();
+ }
+ } catch (e) {}
+ gDialog.BorderWidthInput = document.getElementById("BorderWidthInput");
+ gDialog.SpacingInput = document.getElementById("SpacingInput");
+ gDialog.PaddingInput = document.getElementById("PaddingInput");
+ gDialog.TableAlignList = document.getElementById("TableAlignList");
+ gDialog.TableCaptionList = document.getElementById("TableCaptionList");
+ gDialog.TableInheritColor = document.getElementById("TableInheritColor");
+ gDialog.TabBox = document.getElementById("TabBox");
+
+ // Cell Panel
+ gDialog.SelectionList = document.getElementById("SelectionList");
+ gDialog.PreviousButton = document.getElementById("PreviousButton");
+ gDialog.NextButton = document.getElementById("NextButton");
+ // Currently, we always apply changes and load new attributes when changing selection
+ // (Let's keep this for possible future use)
+ // gDialog.ApplyBeforeMove = document.getElementById("ApplyBeforeMove");
+ // gDialog.KeepCurrentData = document.getElementById("KeepCurrentData");
+
+ gDialog.CellHeightInput = document.getElementById("CellHeightInput");
+ gDialog.CellHeightUnits = document.getElementById("CellHeightUnits");
+ gDialog.CellWidthInput = document.getElementById("CellWidthInput");
+ gDialog.CellWidthUnits = document.getElementById("CellWidthUnits");
+ gDialog.CellHAlignList = document.getElementById("CellHAlignList");
+ gDialog.CellVAlignList = document.getElementById("CellVAlignList");
+ gDialog.CellInheritColor = document.getElementById("CellInheritColor");
+ gDialog.CellStyleList = document.getElementById("CellStyleList");
+ gDialog.TextWrapList = document.getElementById("TextWrapList");
+
+ // In cell panel, user must tell us which attributes to apply via checkboxes,
+ // else we would apply values from one cell to ALL in selection
+ // and that's probably not what they expect!
+ gDialog.CellHeightCheckbox = document.getElementById("CellHeightCheckbox");
+ gDialog.CellWidthCheckbox = document.getElementById("CellWidthCheckbox");
+ gDialog.CellHAlignCheckbox = document.getElementById("CellHAlignCheckbox");
+ gDialog.CellVAlignCheckbox = document.getElementById("CellVAlignCheckbox");
+ gDialog.CellStyleCheckbox = document.getElementById("CellStyleCheckbox");
+ gDialog.TextWrapCheckbox = document.getElementById("TextWrapCheckbox");
+ gDialog.CellColorCheckbox = document.getElementById("CellColorCheckbox");
+ gDialog.TableTab = document.getElementById("TableTab");
+ gDialog.CellTab = document.getElementById("CellTab");
+ gDialog.AdvancedEditCell = document.getElementById("AdvancedEditButton2");
+ // Save "normal" tooltip message for Advanced Edit button
+ gDialog.AdvancedEditCellToolTipText =
+ gDialog.AdvancedEditCell.getAttribute("tooltiptext");
+
+ try {
+ gTableElement = gActiveEditor.getElementOrParentByTagName("table", null);
+ } catch (e) {}
+ if (!gTableElement) {
+ dump("Failed to get table element!\n");
+ window.close();
+ return;
+ }
+ globalTableElement = gTableElement.cloneNode(false);
+
+ var tagNameObj = { value: "" };
+ var countObj = { value: 0 };
+ var tableOrCellElement;
+ try {
+ tableOrCellElement = gActiveEditor.getSelectedOrParentTableElement(
+ tagNameObj,
+ countObj
+ );
+ } catch (e) {}
+
+ if (tagNameObj.value == "td") {
+ // We are in a cell
+ gSelectedCellCount = countObj.value;
+ gCellElement = tableOrCellElement;
+ globalCellElement = gCellElement.cloneNode(false);
+
+ // Tells us whether cell, row, or column is selected
+ try {
+ gSelectedCellsType = gActiveEditor.getSelectedCellsType(gTableElement);
+ } catch (e) {}
+
+ // Ignore types except Cell, Row, and Column
+ if (
+ gSelectedCellsType < SELECT_CELL ||
+ gSelectedCellsType > SELECT_COLUMN
+ ) {
+ gSelectedCellsType = SELECT_CELL;
+ }
+
+ // Be sure at least 1 cell is selected.
+ // (If the count is 0, then we were inside the cell.)
+ if (gSelectedCellCount == 0) {
+ DoCellSelection();
+ }
+
+ // Get location in the cell map
+ var rowIndexObj = { value: 0 };
+ var colIndexObj = { value: 0 };
+ try {
+ gActiveEditor.getCellIndexes(gCellElement, rowIndexObj, colIndexObj);
+ } catch (e) {}
+ gCurRowIndex = rowIndexObj.value;
+ gCurColIndex = colIndexObj.value;
+
+ // We save the current colspan to quickly
+ // move selection from from cell to cell
+ if (GetCellData(gCurRowIndex, gCurColIndex)) {
+ gCurColSpan = gCellData.colSpan;
+ }
+
+ // Starting TabPanel name is passed in
+ if (window.arguments[1] == "CellPanel") {
+ gDialog.TabBox.selectedTab = gDialog.CellTab;
+ }
+ }
+
+ if (gDialog.TabBox.selectedTab == gDialog.TableTab) {
+ // We may call this with table selected, but no cell,
+ // so disable the Cell Properties tab
+ if (!gCellElement) {
+ // XXX: Disabling of tabs is currently broken, so for
+ // now we'll just remove the tab completely.
+ // gDialog.CellTab.disabled = true;
+ gDialog.CellTab.remove();
+ }
+ }
+
+ // Note: we must use gTableElement, not globalTableElement for these,
+ // thus we should not put this in InitDialog.
+ // Instead, monitor desired counts with separate globals
+ var rowCountObj = { value: 0 };
+ var colCountObj = { value: 0 };
+ try {
+ gActiveEditor.getTableSize(gTableElement, rowCountObj, colCountObj);
+ } catch (e) {}
+
+ gRowCount = rowCountObj.value;
+ gLastRowIndex = gRowCount - 1;
+ gColCount = colCountObj.value;
+ gLastColIndex = gColCount - 1;
+
+ // Set appropriate icons and enable state for the Previous/Next buttons
+ SetSelectionButtons();
+
+ // If only one cell in table, disable change-selection widgets
+ if (gRowCount == 1 && gColCount == 1) {
+ gDialog.SelectionList.setAttribute("disabled", "true");
+ }
+
+ // User can change these via textboxes
+ gNewRowCount = gRowCount;
+ gNewColCount = gColCount;
+
+ // This flag is used to control whether set check state
+ // on "set attribute" checkboxes
+ // (Advanced Edit dialog use calls InitDialog when done)
+ gAdvancedEditUsed = false;
+ InitDialog();
+ gAdvancedEditUsed = true;
+
+ // If first initializing, we really aren't changing anything
+ gCellDataChanged = false;
+
+ SetWindowLocation();
+}
+
+function InitDialog() {
+ // Get Table attributes
+ gDialog.TableRowsInput.value = gRowCount;
+ gDialog.TableColumnsInput.value = gColCount;
+ gDialog.TableWidthInput.value = InitPixelOrPercentMenulist(
+ globalTableElement,
+ gTableElement,
+ "width",
+ "TableWidthUnits",
+ gPercent
+ );
+ if (gUseCSS) {
+ gDialog.TableHeightInput.value = InitPixelOrPercentMenulist(
+ globalTableElement,
+ gTableElement,
+ "height",
+ "TableHeightUnits",
+ gPercent
+ );
+ }
+ gDialog.BorderWidthInput.value = globalTableElement.border;
+ gDialog.SpacingInput.value = globalTableElement.cellSpacing;
+ gDialog.PaddingInput.value = globalTableElement.cellPadding;
+
+ var marginLeft = GetHTMLOrCSSStyleValue(
+ globalTableElement,
+ "align",
+ "margin-left"
+ );
+ var marginRight = GetHTMLOrCSSStyleValue(
+ globalTableElement,
+ "align",
+ "margin-right"
+ );
+ var halign = marginLeft.toLowerCase() + " " + marginRight.toLowerCase();
+ if (halign == "center center" || halign == "auto auto") {
+ gDialog.TableAlignList.value = "center";
+ } else if (halign == "right right" || halign == "auto 0px") {
+ gDialog.TableAlignList.value = "right";
+ } else {
+ // Default is left.
+ gDialog.TableAlignList.value = "left";
+ }
+
+ // Be sure to get caption from table in doc, not the copied "globalTableElement"
+ gTableCaptionElement = gTableElement.caption;
+ if (gTableCaptionElement) {
+ var align = GetHTMLOrCSSStyleValue(
+ gTableCaptionElement,
+ "align",
+ "caption-side"
+ );
+ if (align != "bottom" && align != "left" && align != "right") {
+ align = "top";
+ }
+ gDialog.TableCaptionList.value = align;
+ }
+
+ gTableColor = GetHTMLOrCSSStyleValue(
+ globalTableElement,
+ bgcolor,
+ cssBackgroundColorStr
+ );
+ gTableColor = ConvertRGBColorIntoHEXColor(gTableColor);
+ SetColor("tableBackgroundCW", gTableColor);
+
+ InitCellPanel();
+}
+
+function InitCellPanel() {
+ // Get cell attributes
+ if (globalCellElement) {
+ // This assumes order of items is Cell, Row, Column
+ gDialog.SelectionList.value = gSelectedCellsType;
+
+ var previousValue = gDialog.CellHeightInput.value;
+ gDialog.CellHeightInput.value = InitPixelOrPercentMenulist(
+ globalCellElement,
+ gCellElement,
+ "height",
+ "CellHeightUnits",
+ gPixel
+ );
+ gDialog.CellHeightCheckbox.checked =
+ gAdvancedEditUsed && previousValue != gDialog.CellHeightInput.value;
+
+ previousValue = gDialog.CellWidthInput.value;
+ gDialog.CellWidthInput.value = InitPixelOrPercentMenulist(
+ globalCellElement,
+ gCellElement,
+ "width",
+ "CellWidthUnits",
+ gPixel
+ );
+ gDialog.CellWidthCheckbox.checked =
+ gAdvancedEditUsed && previousValue != gDialog.CellWidthInput.value;
+
+ var previousIndex = gDialog.CellVAlignList.selectedIndex;
+ var valign = GetHTMLOrCSSStyleValue(
+ globalCellElement,
+ "valign",
+ "vertical-align"
+ ).toLowerCase();
+ if (valign == topStr || valign == bottomStr) {
+ gDialog.CellVAlignList.value = valign;
+ } else {
+ // Default is middle.
+ gDialog.CellVAlignList.value = defVAlign;
+ }
+
+ gDialog.CellVAlignCheckbox.checked =
+ gAdvancedEditUsed &&
+ previousIndex != gDialog.CellVAlignList.selectedIndex;
+
+ previousIndex = gDialog.CellHAlignList.selectedIndex;
+
+ gAlignWasChar = false;
+
+ var halign = GetHTMLOrCSSStyleValue(
+ globalCellElement,
+ "align",
+ "text-align"
+ ).toLowerCase();
+ switch (halign) {
+ case centerStr:
+ case rightStr:
+ case justifyStr:
+ gDialog.CellHAlignList.value = halign;
+ break;
+ case charStr:
+ // We don't support UI for this because layout doesn't work: bug 2212.
+ // Remember that's what they had so we don't change it
+ // unless they change the alignment by using the menulist
+ gAlignWasChar = true;
+ // Fall through to use show default alignment in menu
+ default:
+ // Default depends on cell type (TH is "center", TD is "left")
+ gDialog.CellHAlignList.value =
+ globalCellElement.nodeName.toLowerCase() == "th" ? "center" : "left";
+ break;
+ }
+
+ gDialog.CellHAlignCheckbox.checked =
+ gAdvancedEditUsed &&
+ previousIndex != gDialog.CellHAlignList.selectedIndex;
+
+ previousIndex = gDialog.CellStyleList.selectedIndex;
+ gDialog.CellStyleList.value = globalCellElement.nodeName.toLowerCase();
+ gDialog.CellStyleCheckbox.checked =
+ gAdvancedEditUsed && previousIndex != gDialog.CellStyleList.selectedIndex;
+
+ previousIndex = gDialog.TextWrapList.selectedIndex;
+ if (
+ GetHTMLOrCSSStyleValue(globalCellElement, "nowrap", "white-space") ==
+ "nowrap"
+ ) {
+ gDialog.TextWrapList.value = "nowrap";
+ } else {
+ gDialog.TextWrapList.value = "wrap";
+ }
+ gDialog.TextWrapCheckbox.checked =
+ gAdvancedEditUsed && previousIndex != gDialog.TextWrapList.selectedIndex;
+
+ previousValue = gCellColor;
+ gCellColor = GetHTMLOrCSSStyleValue(
+ globalCellElement,
+ bgcolor,
+ cssBackgroundColorStr
+ );
+ gCellColor = ConvertRGBColorIntoHEXColor(gCellColor);
+ SetColor("cellBackgroundCW", gCellColor);
+ gDialog.CellColorCheckbox.checked =
+ gAdvancedEditUsed && previousValue != gCellColor;
+
+ // We want to set this true in case changes came
+ // from Advanced Edit dialog session (must assume something changed)
+ gCellDataChanged = true;
+ }
+}
+
+function GetCellData(rowIndex, colIndex) {
+ // Get actual rowspan and colspan
+ var startRowIndexObj = { value: 0 };
+ var startColIndexObj = { value: 0 };
+ var rowSpanObj = { value: 0 };
+ var colSpanObj = { value: 0 };
+ var actualRowSpanObj = { value: 0 };
+ var actualColSpanObj = { value: 0 };
+ var isSelectedObj = { value: false };
+
+ try {
+ gActiveEditor.getCellDataAt(
+ gTableElement,
+ rowIndex,
+ colIndex,
+ gCellData,
+ startRowIndexObj,
+ startColIndexObj,
+ rowSpanObj,
+ colSpanObj,
+ actualRowSpanObj,
+ actualColSpanObj,
+ isSelectedObj
+ );
+ // We didn't find a cell
+ if (!gCellData.value) {
+ return false;
+ }
+ } catch (ex) {
+ return false;
+ }
+
+ gCellData.startRowIndex = startRowIndexObj.value;
+ gCellData.startColIndex = startColIndexObj.value;
+ gCellData.rowSpan = rowSpanObj.value;
+ gCellData.colSpan = colSpanObj.value;
+ gCellData.actualRowSpan = actualRowSpanObj.value;
+ gCellData.actualColSpan = actualColSpanObj.value;
+ gCellData.isSelected = isSelectedObj.value;
+ return true;
+}
+
+function SelectCellHAlign() {
+ SetCheckbox("CellHAlignCheckbox");
+ // Once user changes the alignment,
+ // we lose their original "CharAt" alignment"
+ gAlignWasChar = false;
+}
+
+function GetColorAndUpdate(ColorWellID) {
+ var colorWell = document.getElementById(ColorWellID);
+ if (!colorWell) {
+ return;
+ }
+
+ var colorObj = {
+ Type: "",
+ TableColor: 0,
+ CellColor: 0,
+ NoDefault: false,
+ Cancel: false,
+ BackgroundColor: 0,
+ };
+
+ switch (ColorWellID) {
+ case "tableBackgroundCW":
+ colorObj.Type = "Table";
+ colorObj.TableColor = gTableColor;
+ break;
+ case "cellBackgroundCW":
+ colorObj.Type = "Cell";
+ colorObj.CellColor = gCellColor;
+ break;
+ }
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdColorPicker.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ "",
+ colorObj
+ );
+
+ // User canceled the dialog
+ if (colorObj.Cancel) {
+ return;
+ }
+
+ switch (ColorWellID) {
+ case "tableBackgroundCW":
+ gTableColor = colorObj.BackgroundColor;
+ SetColor(ColorWellID, gTableColor);
+ break;
+ case "cellBackgroundCW":
+ gCellColor = colorObj.BackgroundColor;
+ SetColor(ColorWellID, gCellColor);
+ SetCheckbox("CellColorCheckbox");
+ break;
+ }
+}
+
+function SetColor(ColorWellID, color) {
+ // Save the color
+ if (ColorWellID == "cellBackgroundCW") {
+ if (color) {
+ try {
+ gActiveEditor.setAttributeOrEquivalent(
+ globalCellElement,
+ bgcolor,
+ color,
+ true
+ );
+ } catch (e) {}
+ gDialog.CellInheritColor.collapsed = true;
+ } else {
+ try {
+ gActiveEditor.removeAttributeOrEquivalent(
+ globalCellElement,
+ bgcolor,
+ true
+ );
+ } catch (e) {}
+ // Reveal addition message explaining "default" color
+ gDialog.CellInheritColor.collapsed = false;
+ }
+ } else {
+ if (color) {
+ try {
+ gActiveEditor.setAttributeOrEquivalent(
+ globalTableElement,
+ bgcolor,
+ color,
+ true
+ );
+ } catch (e) {}
+ gDialog.TableInheritColor.collapsed = true;
+ } else {
+ try {
+ gActiveEditor.removeAttributeOrEquivalent(
+ globalTableElement,
+ bgcolor,
+ true
+ );
+ } catch (e) {}
+ gDialog.TableInheritColor.collapsed = false;
+ }
+ SetCheckbox("CellColorCheckbox");
+ }
+
+ setColorWell(ColorWellID, color);
+}
+
+function ChangeSelectionToFirstCell() {
+ if (!GetCellData(0, 0)) {
+ dump("Can't find first cell in table!\n");
+ return;
+ }
+ gCellElement = gCellData.value;
+ globalCellElement = gCellElement;
+
+ gCurRowIndex = 0;
+ gCurColIndex = 0;
+ ChangeSelection(RESET_SELECTION);
+}
+
+function ChangeSelection(newType) {
+ newType = Number(newType);
+
+ if (gSelectedCellsType == newType) {
+ return;
+ }
+
+ if (newType == RESET_SELECTION) {
+ // Restore selection to existing focus cell
+ gSelection.collapse(gCellElement, 0);
+ } else {
+ gSelectedCellsType = newType;
+ }
+
+ // Keep the same focus gCellElement, just change the type
+ DoCellSelection();
+ SetSelectionButtons();
+
+ // Note: globalCellElement should still be a clone of gCellElement
+}
+
+function MoveSelection(forward) {
+ var newRowIndex = gCurRowIndex;
+ var newColIndex = gCurColIndex;
+ var inRow = false;
+
+ if (gSelectedCellsType == SELECT_ROW) {
+ newRowIndex += forward ? 1 : -1;
+
+ // Wrap around if before first or after last row
+ if (newRowIndex < 0) {
+ newRowIndex = gLastRowIndex;
+ } else if (newRowIndex > gLastRowIndex) {
+ newRowIndex = 0;
+ }
+ inRow = true;
+
+ // Use first cell in row for focus cell
+ newColIndex = 0;
+ } else {
+ // Cell or column:
+ if (!forward) {
+ newColIndex--;
+ }
+
+ if (gSelectedCellsType == SELECT_CELL) {
+ // Skip to next cell
+ if (forward) {
+ newColIndex += gCurColSpan;
+ }
+ } else {
+ // SELECT_COLUMN
+ // Use first cell in column for focus cell
+ newRowIndex = 0;
+
+ // Don't skip by colspan,
+ // but find first cell in next cellmap column
+ if (forward) {
+ newColIndex++;
+ }
+ }
+
+ if (newColIndex < 0) {
+ // Request is before the first cell in column
+
+ // Wrap to last cell in column
+ newColIndex = gLastColIndex;
+
+ if (gSelectedCellsType == SELECT_CELL) {
+ // If moving by cell, also wrap to previous...
+ if (newRowIndex > 0) {
+ newRowIndex -= 1;
+ } else {
+ // ...or the last row.
+ newRowIndex = gLastRowIndex;
+ }
+
+ inRow = true;
+ }
+ } else if (newColIndex > gLastColIndex) {
+ // Request is after the last cell in column
+
+ // Wrap to first cell in column
+ newColIndex = 0;
+
+ if (gSelectedCellsType == SELECT_CELL) {
+ // If moving by cell, also wrap to next...
+ if (newRowIndex < gLastRowIndex) {
+ newRowIndex++;
+ } else {
+ // ...or the first row.
+ newRowIndex = 0;
+ }
+
+ inRow = true;
+ }
+ }
+ }
+
+ // Get the cell at the new location
+ do {
+ if (!GetCellData(newRowIndex, newColIndex)) {
+ dump("MoveSelection: CELL NOT FOUND\n");
+ return;
+ }
+ if (inRow) {
+ if (gCellData.startRowIndex == newRowIndex) {
+ break;
+ } else {
+ // Cell spans from a row above, look for the next cell in row.
+ newRowIndex += gCellData.actualRowSpan;
+ }
+ } else if (gCellData.startColIndex == newColIndex) {
+ break;
+ } else {
+ // Cell spans from a Col above, look for the next cell in column
+ newColIndex += gCellData.actualColSpan;
+ }
+ } while (true);
+
+ // Save data for current selection before changing
+ if (gCellDataChanged) {
+ // && gDialog.ApplyBeforeMove.checked)
+ if (!ValidateCellData()) {
+ return;
+ }
+
+ gActiveEditor.beginTransaction();
+ // Apply changes to all selected cells
+ ApplyCellAttributes();
+ gActiveEditor.endTransaction();
+
+ SetCloseButton();
+ }
+
+ // Set cell and other data for new selection
+ gCellElement = gCellData.value;
+
+ // Save globals for new current cell
+ gCurRowIndex = gCellData.startRowIndex;
+ gCurColIndex = gCellData.startColIndex;
+ gCurColSpan = gCellData.actualColSpan;
+
+ // Copy for new global cell
+ globalCellElement = gCellElement.cloneNode(false);
+
+ // Change the selection
+ DoCellSelection();
+
+ // Scroll page so new selection is visible
+ // Using SELECTION_ANCHOR_REGION makes the upper-left corner of first selected cell
+ // the point to bring into view.
+ try {
+ var selectionController = gActiveEditor.selectionController;
+ selectionController.scrollSelectionIntoView(
+ selectionController.SELECTION_NORMAL,
+ selectionController.SELECTION_ANCHOR_REGION,
+ true
+ );
+ } catch (e) {}
+
+ // Reinitialize dialog using new cell
+ // if (!gDialog.KeepCurrentData.checked)
+ // Setting this false unchecks all "set attributes" checkboxes
+ gAdvancedEditUsed = false;
+ InitCellPanel();
+ gAdvancedEditUsed = true;
+}
+
+function DoCellSelection() {
+ // Collapse selection into to the focus cell
+ // so editor uses that as start cell
+ gSelection.collapse(gCellElement, 0);
+
+ var tagNameObj = { value: "" };
+ var countObj = { value: 0 };
+ try {
+ switch (gSelectedCellsType) {
+ case SELECT_CELL:
+ gActiveEditor.selectTableCell();
+ break;
+ case SELECT_ROW:
+ gActiveEditor.selectTableRow();
+ break;
+ default:
+ gActiveEditor.selectTableColumn();
+ break;
+ }
+ // Get number of cells selected
+ gActiveEditor.getSelectedOrParentTableElement(tagNameObj, countObj);
+ } catch (e) {}
+
+ if (tagNameObj.value == "td") {
+ gSelectedCellCount = countObj.value;
+ } else {
+ gSelectedCellCount = 0;
+ }
+
+ // Currently, we can only allow advanced editing on ONE cell element at a time
+ // else we ignore CSS, JS, and HTML attributes not already in dialog
+ SetElementEnabled(gDialog.AdvancedEditCell, gSelectedCellCount == 1);
+
+ gDialog.AdvancedEditCell.setAttribute(
+ "tooltiptext",
+ gSelectedCellCount > 1
+ ? GetString("AdvancedEditForCellMsg")
+ : gDialog.AdvancedEditCellToolTipText
+ );
+}
+
+function SetSelectionButtons() {
+ if (gSelectedCellsType == SELECT_ROW) {
+ // Trigger CSS to set images of up and down arrows
+ gDialog.PreviousButton.setAttribute("type", "row");
+ gDialog.NextButton.setAttribute("type", "row");
+ } else {
+ // or images of left and right arrows
+ gDialog.PreviousButton.setAttribute("type", "col");
+ gDialog.NextButton.setAttribute("type", "col");
+ }
+ DisableSelectionButtons(
+ (gSelectedCellsType == SELECT_ROW && gRowCount == 1) ||
+ (gSelectedCellsType == SELECT_COLUMN && gColCount == 1) ||
+ (gRowCount == 1 && gColCount == 1)
+ );
+}
+
+function DisableSelectionButtons(disable) {
+ gDialog.PreviousButton.setAttribute("disabled", disable ? "true" : "false");
+ gDialog.NextButton.setAttribute("disabled", disable ? "true" : "false");
+}
+
+function SwitchToValidatePanel() {
+ if (gDialog.TabBox.selectedTab != gValidateTab) {
+ gDialog.TabBox.selectedTab = gValidateTab;
+ }
+}
+
+function SetAlign(listID, defaultValue, element, attName) {
+ var value = document.getElementById(listID).value;
+ if (value == defaultValue) {
+ try {
+ gActiveEditor.removeAttributeOrEquivalent(element, attName, true);
+ } catch (e) {}
+ } else {
+ try {
+ gActiveEditor.setAttributeOrEquivalent(element, attName, value, true);
+ } catch (e) {}
+ }
+}
+
+function ValidateTableData() {
+ gValidateTab = gDialog.TableTab;
+ gNewRowCount = Number(
+ ValidateNumber(gDialog.TableRowsInput, null, 1, gMaxRows, null, true, true)
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ gNewColCount = Number(
+ ValidateNumber(
+ gDialog.TableColumnsInput,
+ null,
+ 1,
+ gMaxColumns,
+ null,
+ true,
+ true
+ )
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ // If user is deleting any cells, get confirmation
+ // (This is a global to the dialog and we ask only once per dialog session)
+ if (!gCanDelete && (gNewRowCount < gRowCount || gNewColCount < gColCount)) {
+ if (
+ ConfirmWithTitle(
+ GetString("DeleteTableTitle"),
+ GetString("DeleteTableMsg"),
+ GetString("DeleteCells")
+ )
+ ) {
+ gCanDelete = true;
+ } else {
+ SetTextboxFocus(
+ gNewRowCount < gRowCount
+ ? gDialog.TableRowsInput
+ : gDialog.TableColumnsInput
+ );
+ return false;
+ }
+ }
+
+ ValidateNumber(
+ gDialog.TableWidthInput,
+ gDialog.TableWidthUnits,
+ 1,
+ gMaxTableSize,
+ globalTableElement,
+ "width"
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ if (gUseCSS) {
+ ValidateNumber(
+ gDialog.TableHeightInput,
+ gDialog.TableHeightUnits,
+ 1,
+ gMaxTableSize,
+ globalTableElement,
+ "height"
+ );
+ if (gValidationError) {
+ return false;
+ }
+ }
+
+ ValidateNumber(
+ gDialog.BorderWidthInput,
+ null,
+ 0,
+ gMaxPixels,
+ globalTableElement,
+ "border"
+ );
+ // TODO: Deal with "BORDER" without value issue
+ if (gValidationError) {
+ return false;
+ }
+
+ ValidateNumber(
+ gDialog.SpacingInput,
+ null,
+ 0,
+ gMaxPixels,
+ globalTableElement,
+ "cellspacing"
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ ValidateNumber(
+ gDialog.PaddingInput,
+ null,
+ 0,
+ gMaxPixels,
+ globalTableElement,
+ "cellpadding"
+ );
+ if (gValidationError) {
+ return false;
+ }
+
+ SetAlign("TableAlignList", defHAlign, globalTableElement, "align");
+
+ // Color is set on globalCellElement immediately
+ return true;
+}
+
+function ValidateCellData() {
+ gValidateTab = gDialog.CellTab;
+
+ if (gDialog.CellHeightCheckbox.checked) {
+ ValidateNumber(
+ gDialog.CellHeightInput,
+ gDialog.CellHeightUnits,
+ 1,
+ gMaxTableSize,
+ globalCellElement,
+ "height"
+ );
+ if (gValidationError) {
+ return false;
+ }
+ }
+
+ if (gDialog.CellWidthCheckbox.checked) {
+ ValidateNumber(
+ gDialog.CellWidthInput,
+ gDialog.CellWidthUnits,
+ 1,
+ gMaxTableSize,
+ globalCellElement,
+ "width"
+ );
+ if (gValidationError) {
+ return false;
+ }
+ }
+
+ if (gDialog.CellHAlignCheckbox.checked) {
+ var hAlign = gDialog.CellHAlignList.value;
+
+ // Horizontal alignment is complicated by "char" type
+ // We don't change current values if user didn't edit alignment
+ if (!gAlignWasChar) {
+ globalCellElement.removeAttribute(charStr);
+
+ // Always set "align" attribute,
+ // so the default "left" is effective in a cell
+ // when parent row has align set.
+ globalCellElement.setAttribute("align", hAlign);
+ }
+ }
+
+ if (gDialog.CellVAlignCheckbox.checked) {
+ // Always set valign (no default in 2nd param) so
+ // the default "middle" is effective in a cell
+ // when parent row has valign set.
+ SetAlign("CellVAlignList", "", globalCellElement, "valign");
+ }
+
+ if (gDialog.TextWrapCheckbox.checked) {
+ if (gDialog.TextWrapList.value == "nowrap") {
+ try {
+ gActiveEditor.setAttributeOrEquivalent(
+ globalCellElement,
+ "nowrap",
+ "nowrap",
+ true
+ );
+ } catch (e) {}
+ } else {
+ try {
+ gActiveEditor.removeAttributeOrEquivalent(
+ globalCellElement,
+ "nowrap",
+ true
+ );
+ } catch (e) {}
+ }
+ }
+
+ return true;
+}
+
+function ValidateData() {
+ var result;
+
+ // Validate current panel first
+ if (gDialog.TabBox.selectedTab == gDialog.TableTab) {
+ result = ValidateTableData();
+ if (result) {
+ result = ValidateCellData();
+ }
+ } else {
+ result = ValidateCellData();
+ if (result) {
+ result = ValidateTableData();
+ }
+ }
+ if (!result) {
+ return false;
+ }
+
+ // Set global element for AdvancedEdit
+ if (gDialog.TabBox.selectedTab == gDialog.TableTab) {
+ globalElement = globalTableElement;
+ } else {
+ globalElement = globalCellElement;
+ }
+
+ return true;
+}
+
+function ChangeCellTextbox(textboxID) {
+ // Filter input for just integers
+ forceInteger(textboxID);
+
+ if (gDialog.TabBox.selectedTab == gDialog.CellTab) {
+ gCellDataChanged = true;
+ }
+}
+
+// Call this when a textbox or menulist is changed
+// so the checkbox is automatically set
+function SetCheckbox(checkboxID) {
+ if (checkboxID && checkboxID.length > 0) {
+ // Set associated checkbox
+ document.getElementById(checkboxID).checked = true;
+ }
+ gCellDataChanged = true;
+}
+
+function ChangeIntTextbox(checkboxID) {
+ // Set associated checkbox
+ SetCheckbox(checkboxID);
+}
+
+function CloneAttribute(destElement, srcElement, attr) {
+ var value = srcElement.getAttribute(attr);
+ // Use editor methods since we are always
+ // modifying a table in the document and
+ // we need transaction system for undo
+ try {
+ if (!value || value.length == 0) {
+ gActiveEditor.removeAttributeOrEquivalent(destElement, attr, false);
+ } else {
+ gActiveEditor.setAttributeOrEquivalent(destElement, attr, value, false);
+ }
+ } catch (e) {}
+}
+
+/* eslint-disable complexity */
+function ApplyTableAttributes() {
+ var newAlign = gDialog.TableCaptionList.value;
+ if (!newAlign) {
+ newAlign = "";
+ }
+
+ if (gTableCaptionElement) {
+ // Get current alignment
+ var align = GetHTMLOrCSSStyleValue(
+ gTableCaptionElement,
+ "align",
+ "caption-side"
+ ).toLowerCase();
+ // This is the default
+ if (!align) {
+ align = "top";
+ }
+
+ if (newAlign == "") {
+ // Remove existing caption
+ try {
+ gActiveEditor.deleteNode(gTableCaptionElement);
+ } catch (e) {}
+ gTableCaptionElement = null;
+ } else if (newAlign != align) {
+ try {
+ if (newAlign == "top") {
+ // This is default, so don't explicitly set it
+ gActiveEditor.removeAttributeOrEquivalent(
+ gTableCaptionElement,
+ "align",
+ false
+ );
+ } else {
+ gActiveEditor.setAttributeOrEquivalent(
+ gTableCaptionElement,
+ "align",
+ newAlign,
+ false
+ );
+ }
+ } catch (e) {}
+ }
+ } else if (newAlign != "") {
+ // Create and insert a caption:
+ try {
+ gTableCaptionElement = gActiveEditor.createElementWithDefaults("caption");
+ } catch (e) {}
+ if (gTableCaptionElement) {
+ if (newAlign != "top") {
+ gTableCaptionElement.setAttribute("align", newAlign);
+ }
+
+ // Insert it into the table - caption is always inserted as first child
+ try {
+ gActiveEditor.insertNode(gTableCaptionElement, gTableElement, 0);
+ } catch (e) {}
+
+ // Put selection back where it was
+ ChangeSelection(RESET_SELECTION);
+ }
+ }
+
+ var countDelta;
+ var foundCell;
+ var i;
+
+ if (gNewRowCount != gRowCount) {
+ countDelta = gNewRowCount - gRowCount;
+ if (gNewRowCount > gRowCount) {
+ // Append new rows
+ // Find first cell in last row
+ if (GetCellData(gLastRowIndex, 0)) {
+ try {
+ // Move selection to the last cell
+ gSelection.collapse(gCellData.value, 0);
+ // Insert new rows after it
+ gActiveEditor.insertTableRow(countDelta, true);
+ gRowCount = gNewRowCount;
+ gLastRowIndex = gRowCount - 1;
+ // Put selection back where it was
+ ChangeSelection(RESET_SELECTION);
+ } catch (ex) {
+ dump("FAILED TO FIND FIRST CELL IN LAST ROW\n");
+ }
+ }
+ } else if (gCanDelete) {
+ // Delete rows
+ // Find first cell starting in first row we delete
+ var firstDeleteRow = gRowCount + countDelta;
+ foundCell = false;
+ for (i = 0; i <= gLastColIndex; i++) {
+ if (!GetCellData(firstDeleteRow, i)) {
+ // We failed to find a cell.
+ break;
+ }
+
+ if (gCellData.startRowIndex == firstDeleteRow) {
+ foundCell = true;
+ break;
+ }
+ }
+ if (foundCell) {
+ try {
+ // Move selection to the cell we found
+ gSelection.collapse(gCellData.value, 0);
+ gActiveEditor.deleteTableRow(-countDelta);
+ gRowCount = gNewRowCount;
+ gLastRowIndex = gRowCount - 1;
+ if (gCurRowIndex > gLastRowIndex) {
+ // We are deleting our selection
+ // move it to start of table
+ ChangeSelectionToFirstCell();
+ } else {
+ // Put selection back where it was.
+ ChangeSelection(RESET_SELECTION);
+ }
+ } catch (ex) {
+ dump("FAILED TO FIND FIRST CELL IN LAST ROW\n");
+ }
+ }
+ }
+ }
+
+ if (gNewColCount != gColCount) {
+ countDelta = gNewColCount - gColCount;
+
+ if (gNewColCount > gColCount) {
+ // Append new columns
+ // Find last cell in first column
+ if (GetCellData(0, gLastColIndex)) {
+ try {
+ // Move selection to the last cell
+ gSelection.collapse(gCellData.value, 0);
+ gActiveEditor.insertTableColumn(countDelta, true);
+ gColCount = gNewColCount;
+ gLastColIndex = gColCount - 1;
+ // Restore selection
+ ChangeSelection(RESET_SELECTION);
+ } catch (ex) {
+ dump("FAILED TO FIND FIRST CELL IN LAST COLUMN\n");
+ }
+ }
+ } else if (gCanDelete) {
+ // Delete columns
+ var firstDeleteCol = gColCount + countDelta;
+ foundCell = false;
+ for (i = 0; i <= gLastRowIndex; i++) {
+ // Find first cell starting in first column we delete
+ if (!GetCellData(i, firstDeleteCol)) {
+ // We failed to find a cell.
+ break;
+ }
+
+ if (gCellData.startColIndex == firstDeleteCol) {
+ foundCell = true;
+ break;
+ }
+ }
+ if (foundCell) {
+ try {
+ // Move selection to the cell we found
+ gSelection.collapse(gCellData.value, 0);
+ gActiveEditor.deleteTableColumn(-countDelta);
+ gColCount = gNewColCount;
+ gLastColIndex = gColCount - 1;
+ if (gCurColIndex > gLastColIndex) {
+ ChangeSelectionToFirstCell();
+ } else {
+ ChangeSelection(RESET_SELECTION);
+ }
+ } catch (ex) {
+ dump("FAILED TO FIND FIRST CELL IN LAST ROW\n");
+ }
+ }
+ }
+ }
+
+ // Clone all remaining attributes to pick up
+ // anything changed by Advanced Edit Dialog
+ try {
+ gActiveEditor.cloneAttributes(gTableElement, globalTableElement);
+ } catch (e) {}
+}
+/* eslint-enable complexity */
+
+function ApplyCellAttributes() {
+ let selectedCells = gActiveEditor.getSelectedCells();
+ if (selectedCells.length == 0) {
+ return;
+ }
+
+ if (selectedCells.length == 1) {
+ let cell = selectedCells[0];
+ // When only one cell is selected, simply clone entire element,
+ // thus CSS and JS from Advanced edit is copied
+
+ gActiveEditor.cloneAttributes(cell, globalCellElement);
+
+ if (gDialog.CellStyleCheckbox.checked) {
+ let currentStyleIndex = cell.nodeName.toLowerCase() == "th" ? 1 : 0;
+ if (gDialog.CellStyleList.selectedIndex != currentStyleIndex) {
+ // Switch cell types
+ // (replaces with new cell and copies attributes and contents)
+ gActiveEditor.switchTableCellHeaderType(cell);
+ }
+ }
+ } else {
+ // Apply changes to all selected cells
+ // XXX THIS DOESN'T COPY ADVANCED EDIT CHANGES!
+ for (let cell of selectedCells) {
+ ApplyAttributesToOneCell(cell);
+ }
+ }
+ gCellDataChanged = false;
+}
+
+function ApplyAttributesToOneCell(destElement) {
+ if (gDialog.CellHeightCheckbox.checked) {
+ CloneAttribute(destElement, globalCellElement, "height");
+ }
+
+ if (gDialog.CellWidthCheckbox.checked) {
+ CloneAttribute(destElement, globalCellElement, "width");
+ }
+
+ if (gDialog.CellHAlignCheckbox.checked) {
+ CloneAttribute(destElement, globalCellElement, "align");
+ CloneAttribute(destElement, globalCellElement, charStr);
+ }
+
+ if (gDialog.CellVAlignCheckbox.checked) {
+ CloneAttribute(destElement, globalCellElement, "valign");
+ }
+
+ if (gDialog.TextWrapCheckbox.checked) {
+ CloneAttribute(destElement, globalCellElement, "nowrap");
+ }
+
+ if (gDialog.CellStyleCheckbox.checked) {
+ var newStyleIndex = gDialog.CellStyleList.selectedIndex;
+ var currentStyleIndex = destElement.nodeName.toLowerCase() == "th" ? 1 : 0;
+
+ if (newStyleIndex != currentStyleIndex) {
+ // Switch cell types
+ // (replaces with new cell and copies attributes and contents)
+ try {
+ destElement = gActiveEditor.switchTableCellHeaderType(destElement);
+ } catch (e) {}
+ }
+ }
+
+ if (gDialog.CellColorCheckbox.checked) {
+ CloneAttribute(destElement, globalCellElement, "bgcolor");
+ }
+}
+
+function SetCloseButton() {
+ // Change text on "Cancel" button after Apply is used
+ if (!gApplyUsed) {
+ document
+ .querySelector("dialog")
+ .setAttribute(
+ "buttonlabelcancel",
+ document.querySelector("dialog").getAttribute("buttonlabelclose")
+ );
+ gApplyUsed = true;
+ }
+}
+
+function Apply() {
+ if (ValidateData()) {
+ gActiveEditor.beginTransaction();
+
+ ApplyTableAttributes();
+
+ // We may have just a table, so check for cell element
+ if (globalCellElement) {
+ ApplyCellAttributes();
+ }
+
+ gActiveEditor.endTransaction();
+
+ SetCloseButton();
+ return true;
+ }
+ return false;
+}
+
+function onAccept(event) {
+ // Do same as Apply and close window if ValidateData succeeded
+ var retVal = Apply();
+ if (retVal) {
+ SaveWindowLocation();
+ } else {
+ event.preventDefault();
+ }
+}
diff --git a/comm/mail/components/compose/content/dialogs/EdTableProps.xhtml b/comm/mail/components/compose/content/dialogs/EdTableProps.xhtml
new file mode 100644
index 0000000000..a82d5e18c5
--- /dev/null
+++ b/comm/mail/components/compose/content/dialogs/EdTableProps.xhtml
@@ -0,0 +1,472 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % edTableProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorTableProperties.dtd">
+%edTableProperties;
+<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd">
+%edDialogOverlay; ]>
+
+<window
+ title="&tableWindow.title;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="Startup()"
+>
+ <dialog
+ id="tableDlg"
+ buttons="accept,extra1,cancel"
+ buttonlabelclose="&closeButton.label;"
+ buttonlabelextra1="&applyButton.label;"
+ buttonaccesskeyextra1="&applyButton.accesskey;"
+ >
+ <!-- Methods common to all editor dialogs -->
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://messenger/content/messengercompose/editorUtilities.js" />
+ <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" />
+ <script src="chrome://messenger/content/messengercompose/EdTableProps.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <spacer id="location" offsetY="50" persist="offsetX offsetY" />
+
+ <tabbox id="TabBox">
+ <tabs flex="1">
+ <tab id="TableTab" label="&tableTab.label;" />
+ <tab id="CellTab" label="&cellTab.label;" />
+ </tabs>
+ <tabpanels>
+ <!-- TABLE PANEL -->
+ <vbox>
+ <html:fieldset orient="horizontal">
+ <html:legend>&size.label;</html:legend>
+ <hbox>
+ <vbox>
+ <hbox>
+ <vbox>
+ <hbox align="center" flex="1">
+ <label
+ id="TableRowsLabel"
+ value="&tableRows.label;"
+ accesskey="&tableRows.accessKey;"
+ control="TableRowsInput"
+ />
+ </hbox>
+ <hbox align="center" flex="1">
+ <label
+ id="TableColumnsLabel"
+ value="&tableColumns.label;"
+ accesskey="&tableColumns.accessKey;"
+ control="TableColumnsInput"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <html:input
+ id="TableRowsInput"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="TableRowsLabel"
+ />
+ <html:input
+ id="TableColumnsInput"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="TableColumnsLabel"
+ />
+ </vbox>
+ </hbox>
+ </vbox>
+ <vbox>
+ <html:div class="grid-three-column">
+ <html:div class="flex-items-center">
+ <label
+ id="TableHeightLabel"
+ value="&tableHeight.label;"
+ accesskey="&tableHeight.accessKey;"
+ control="TableHeightInput"
+ />
+ </html:div>
+ <html:div>
+ <html:input
+ id="TableHeightInput"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="TableHeightLabel"
+ />
+ </html:div>
+ <html:div class="flex-items-center">
+ <menulist id="TableHeightUnits" />
+ </html:div>
+ <html:div class="flex-items-center">
+ <label
+ id="TableWidthLabel"
+ value="&tableWidth.label;"
+ accesskey="&tableWidth.accessKey;"
+ control="TableWidthInput"
+ />
+ </html:div>
+ <html:div class="flex-items-center">
+ <html:input
+ id="TableWidthInput"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="TableWidthLabel"
+ />
+ </html:div>
+ <html:div class="flex-items-center">
+ <menulist id="TableWidthUnits" />
+ </html:div>
+ </html:div>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ <html:fieldset>
+ <html:legend>&tableBorderSpacing.label;</html:legend>
+ <hbox>
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ id="BorderWidthLabel"
+ control="BorderWidthInput"
+ value="&tableBorderWidth.label;"
+ accesskey="&tableBorderWidth.accessKey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="SpacingLabel"
+ control="SpacingInput"
+ value="&tableSpacing.label;"
+ accesskey="&tableSpacing.accessKey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="PaddingLabel"
+ control="PaddingInput"
+ value="&tablePadding.label;"
+ accesskey="&tablePadding.accessKey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <html:input
+ id="BorderWidthInput"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="BorderWidthLabel"
+ />
+ <html:input
+ id="SpacingInput"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="SpacingLabel"
+ />
+ <html:input
+ id="PaddingInput"
+ type="number"
+ class="narrow input-inline"
+ aria-labelledby="PaddingLabel"
+ />
+ </vbox>
+ <vbox>
+ <hbox flex="1" align="center">
+ <label align="start" value="&pixels.label;" />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label value="&tablePxBetwCells.label;" />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label value="&tablePxBetwBrdrCellContent.label;" />
+ </hbox>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ <!-- Table Alignment and Caption -->
+ <hbox flex="1" align="center">
+ <label
+ control="TableAlignList"
+ value="&tableAlignment.label;"
+ accesskey="&tableAlignment.accessKey;"
+ />
+ <menulist id="TableAlignList">
+ <menupopup>
+ <menuitem label="&AlignLeft.label;" value="left" />
+ <menuitem label="&AlignCenter.label;" value="center" />
+ <menuitem label="&AlignRight.label;" value="right" />
+ </menupopup>
+ </menulist>
+ <spacer class="spacer" />
+ <label
+ control="TableCaptionList"
+ value="&tableCaption.label;"
+ accesskey="&tableCaption.accessKey;"
+ />
+ <menulist id="TableCaptionList">
+ <menupopup>
+ <menuitem label="&tableCaptionNone.label;" value="" />
+ <menuitem label="&tableCaptionAbove.label;" value="top" />
+ <menuitem label="&tableCaptionBelow.label;" value="bottom" />
+ <menuitem label="&tableCaptionLeft.label;" value="left" />
+ <menuitem label="&tableCaptionRight.label;" value="right" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <separator class="groove" />
+ <hbox align="center">
+ <label value="&backgroundColor.label;" />
+ <button
+ id="tableBackground"
+ class="color-button"
+ oncommand="GetColorAndUpdate('tableBackgroundCW');"
+ >
+ <spacer id="tableBackgroundCW" class="color-well" />
+ </button>
+ <spacer class="spacer" />
+ <label
+ id="TableInheritColor"
+ value="&tableInheritColor.label;"
+ collapsed="true"
+ />
+ </hbox>
+ <separator class="groove" />
+ <hbox flex="1" align="center">
+ <spacer flex="1" />
+ <button
+ id="AdvancedEditButton"
+ oncommand="onAdvancedEdit();"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <spacer flex="1" /> </vbox
+ ><!-- Table Panel -->
+
+ <!-- CELL PANEL -->
+ <vbox>
+ <html:fieldset>
+ <html:legend>&cellSelection.label;</html:legend>
+ <vbox>
+ <menulist
+ id="SelectionList"
+ oncommand="ChangeSelection(event.target.value)"
+ >
+ <menupopup>
+ <!-- JS code assumes order is Cell, Row, Column -->
+ <menuitem label="&cellSelectCell.label;" value="1" />
+ <menuitem label="&cellSelectRow.label;" value="2" />
+ <menuitem label="&cellSelectColumn.label;" value="3" />
+ </menupopup>
+ </menulist>
+ <hbox>
+ <button
+ id="PreviousButton"
+ label="&cellSelectPrevious.label;"
+ accesskey="&cellSelectPrevious.accessKey;"
+ oncommand="MoveSelection(0)"
+ />
+ <button
+ id="NextButton"
+ label="&cellSelectNext.label;"
+ accesskey="&cellSelectNext.accessKey;"
+ oncommand="MoveSelection(1)"
+ />
+ </hbox>
+ <hbox flex="1"> &applyBeforeChange.label; </hbox>
+ </vbox>
+ </html:fieldset>
+
+ <separator class="groove" />
+
+ <hbox align="center">
+ <html:fieldset>
+ <html:legend>&size.label;</html:legend>
+ <hbox>
+ <vbox>
+ <hbox flex="1" align="center">
+ <checkbox
+ id="CellHeightCheckbox"
+ label="&tableHeight.label;"
+ accesskey="&tableHeight.accessKey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox
+ id="CellWidthCheckbox"
+ label="&tableWidth.label;"
+ accesskey="&tableWidth.accessKey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <hbox flex="1" align="center">
+ <html:input
+ id="CellHeightInput"
+ type="number"
+ class="narrow input-inline"
+ onchange="ChangeIntTextbox('CellHeightCheckbox');"
+ aria-labelledby="CellHeightCheckbox"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <html:input
+ id="CellWidthInput"
+ type="number"
+ class="narrow input-inline"
+ onchange="ChangeIntTextbox('CellWidthCheckbox');"
+ aria-labelledby="CellWidthCheckbox"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <hbox flex="1" align="center">
+ <menulist
+ id="CellHeightUnits"
+ oncommand="SetCheckbox('CellHeightCheckbox');"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <menulist
+ id="CellWidthUnits"
+ oncommand="SetCheckbox('CellWidthCheckbox');"
+ />
+ </hbox>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ <html:fieldset>
+ <html:legend>&cellContentAlignment.label;</html:legend>
+ <hbox>
+ <vbox>
+ <hbox align="center" flex="1">
+ <checkbox
+ id="CellVAlignCheckbox"
+ label="&cellVertical.label;"
+ accesskey="&cellVertical.accessKey;"
+ />
+ </hbox>
+ <hbox align="center" flex="1">
+ <checkbox
+ id="CellHAlignCheckbox"
+ label="&cellHorizontal.label;"
+ accesskey="&cellHorizontal.accessKey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist
+ id="CellVAlignList"
+ oncommand="SetCheckbox('CellVAlignCheckbox');"
+ >
+ <menupopup>
+ <menuitem label="&cellAlignTop.label;" value="top" />
+ <menuitem
+ label="&cellAlignMiddle.label;"
+ value="middle"
+ />
+ <menuitem
+ label="&cellAlignBottom.label;"
+ value="bottom"
+ />
+ </menupopup>
+ </menulist>
+ <menulist id="CellHAlignList" oncommand="SelectCellHAlign()">
+ <menupopup>
+ <menuitem label="&AlignLeft.label;" value="left" />
+ <menuitem label="&AlignCenter.label;" value="center" />
+ <menuitem label="&AlignRight.label;" value="right" />
+ <menuitem
+ label="&cellAlignJustify.label;"
+ value="justify"
+ />
+ </menupopup>
+ </menulist>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ </hbox>
+ <spacer class="spacer" />
+ <hbox align="center">
+ <checkbox
+ id="CellStyleCheckbox"
+ label="&cellStyle.label;"
+ accesskey="&cellStyle.accessKey;"
+ />
+ <menulist
+ id="CellStyleList"
+ oncommand="SetCheckbox('CellStyleCheckbox');"
+ >
+ <menupopup>
+ <menuitem label="&cellNormal.label;" value="td" />
+ <menuitem label="&cellHeader.label;" value="th" />
+ </menupopup>
+ </menulist>
+ <spacer flex="1" />
+ <checkbox
+ id="TextWrapCheckbox"
+ label="&cellTextWrap.label;"
+ accesskey="&cellTextWrap.accessKey;"
+ />
+ <menulist
+ id="TextWrapList"
+ oncommand="SetCheckbox('TextWrapCheckbox');"
+ >
+ <menupopup>
+ <menuitem label="&cellWrap.label;" value="wrap" />
+ <menuitem label="&cellNoWrap.label;" value="nowrap" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <separator class="groove" />
+ <hbox align="center">
+ <checkbox
+ id="CellColorCheckbox"
+ label="&backgroundColor.label;"
+ accesskey="&backgroundColor.accessKey;"
+ />
+ <button
+ class="color-button"
+ oncommand="GetColorAndUpdate('cellBackgroundCW');"
+ >
+ <spacer id="cellBackgroundCW" class="color-well" />
+ </button>
+ <spacer class="spacer" />
+ <label
+ id="CellInheritColor"
+ value="&cellInheritColor.label;"
+ collapsed="true"
+ />
+ </hbox>
+ <separator class="groove" />
+ <hbox align="center">
+ <description class="wrap" flex="1" style="width: 1em"
+ >&cellUseCheckboxHelp.label;</description
+ >
+ <button
+ id="AdvancedEditButton2"
+ oncommand="onAdvancedEdit()"
+ label="&AdvancedEditButton.label;"
+ accesskey="&AdvancedEditButton.accessKey;"
+ tooltiptext="&AdvancedEditButton.tooltip;"
+ />
+ </hbox>
+ <spacer flex="1" /> </vbox
+ ><!-- Cell Panel -->
+ </tabpanels>
+ </tabbox>
+ <spacer class="spacer" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/compose/content/editFormatButtons.inc.xhtml b/comm/mail/components/compose/content/editFormatButtons.inc.xhtml
new file mode 100644
index 0000000000..f84b2610e6
--- /dev/null
+++ b/comm/mail/components/compose/content/editFormatButtons.inc.xhtml
@@ -0,0 +1,282 @@
+# 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/.
+
+ <!-- Formatting toolbar items. "value" are HTML tagnames, don't translate -->
+ <menulist id="ParagraphSelect"
+ class="toolbar-focustarget"
+ oncommand="setParagraphState(event);"
+ crop="end"
+ tooltiptext="&ParagraphSelect.tooltip;"
+ observes="cmd_renderedHTMLEnabler">
+ <menupopup id="ParagraphPopup">
+ <menuitem id="toolbarmenu_bodyText" label="&bodyTextCmd.label;" value=""/>
+ <menuitem id="toolbarmenu_paragraph" label="&paragraphParagraphCmd.label;" value="p"/>
+ <menuitem id="toolbarmenu_h1" label="&heading1Cmd.label;" value="h1"/>
+ <menuitem id="toolbarmenu_h2" label="&heading2Cmd.label;" value="h2"/>
+ <menuitem id="toolbarmenu_h3" label="&heading3Cmd.label;" value="h3"/>
+ <menuitem id="toolbarmenu_h4" label="&heading4Cmd.label;" value="h4"/>
+ <menuitem id="toolbarmenu_h5" label="&heading5Cmd.label;" value="h5"/>
+ <menuitem id="toolbarmenu_h6" label="&heading6Cmd.label;" value="h6"/>
+ <menuitem id="toolbarmenu_address" label="&paragraphAddressCmd.label;" value="address"/>
+ <menuitem id="toolbarmenu_pre" label="&paragraphPreformatCmd.label;" value="pre"/>
+ </menupopup>
+ </menulist>
+
+ <!-- "value" are HTML tagnames, don't translate -->
+ <menulist id="FontFaceSelect"
+ class="toolbar-focustarget"
+ oncommand="doStatefulCommand('cmd_fontFace', event.target.value)"
+ crop="center"
+ sizetopopup="pref"
+ tooltiptext="&FontFaceSelect.tooltip;"
+ observes="cmd_renderedHTMLEnabler">
+ <menupopup id="FontFacePopup">
+ <menuitem id="toolbarmenu_fontVarWidth" label="&fontVarWidth.label;" value=""/>
+ <menuitem id="toolbarmenu_fontFixedWidth" label="&fontFixedWidth.label;" value="monospace"/>
+ <menuseparator id="toolbarmenuAfterGenericFontsSeparator"/>
+ <menuitem id="toolbarmenu_fontHelvetica" label="&fontHelvetica.label;"
+ value="Helvetica, Arial, sans-serif"
+ value_parsed="helvetica,arial,sans-serif"/>
+ <menuitem id="toolbarmenu_fontTimes" label="&fontTimes.label;"
+ value="Times New Roman, Times, serif"
+ value_parsed="times new roman,times,serif"/>
+ <menuitem id="toolbarmenu_fontCourier" label="&fontCourier.label;"
+ value="Courier New, Courier, monospace"
+ value_parsed="courier new,courier,monospace"/>
+ <menuseparator id="toolbarmenuAfterDefaultFontsSeparator"
+ class="fontFaceMenuAfterDefaultFonts"/>
+ <menuseparator id="toolbarmenuAfterUsedFontsSeparator"
+ class="fontFaceMenuAfterUsedFonts"
+ hidden="true"/>
+ <!-- Local font face items added here by initLocalFontFaceMenu() -->
+ </menupopup>
+ </menulist>
+
+ <toolbaritem id="color-buttons-container"
+ class="formatting-button"
+ align="center">
+ <stack id="ColorButtons">
+ <box class="color-button" id="BackgroundColorButton"
+ onclick="if (!this.hasAttribute('disabled') || this.getAttribute('disabled') != 'true') { EditorSelectColor('', event); }"
+ tooltiptext="&BackgroundColorButton.tooltip;"
+ observes="cmd_backgroundColor"
+ oncommand="/* See MsgComposeCommands.js::updateAllItems for why this attribute is needed here. */"/>
+ <box class="color-button" id="TextColorButton"
+ onclick="if (!this.hasAttribute('disabled') || this.getAttribute('disabled') != 'true') { EditorSelectColor('Text', event); }"
+ tooltiptext="&TextColorButton.tooltip;"
+ observes="cmd_fontColor"
+ oncommand="/* See MsgComposeCommands.js::updateAllItems for why this attribute is needed here. */"/>
+ </stack>
+ </toolbaritem>
+
+ <toolbarbutton id="AbsoluteFontSizeButton"
+ class="formatting-button"
+ tooltiptext="&absoluteFontSizeToolbarCmd.tooltip;"
+ type="menu"
+ observes="cmd_renderedHTMLEnabler">
+ <menupopup id="AbsoluteFontSizeButtonPopup"
+ onpopupshowing="initFontSizeMenu(this);"
+ oncommand="setFontSize(event)">
+ <menuitem id="toobarmenu_fontSize_x-small"
+ label="&size-tinyCmd.label;"
+ type="radio" name="fontSize"
+ value="1"/>
+ <menuitem id="toobarmenu_fontSize_small"
+ label="&size-smallCmd.label;"
+ type="radio" name="fontSize"
+ value="2"/>
+ <menuitem id="toobarmenu_fontSize_medium"
+ label="&size-mediumCmd.label;"
+ type="radio" name="fontSize"
+ value="3"/>
+ <menuitem id="toobarmenu_fontSize_large"
+ label="&size-largeCmd.label;"
+ type="radio" name="fontSize"
+ value="4"/>
+ <menuitem id="toobarmenu_fontSize_x-large"
+ label="&size-extraLargeCmd.label;"
+ type="radio" name="fontSize"
+ value="5"/>
+ <menuitem id="toobarmenu_fontSize_xx-large"
+ label="&size-hugeCmd.label;"
+ type="radio" name="fontSize"
+ value="6"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarbutton id="DecreaseFontSizeButton"
+ class="formatting-button"
+ tooltiptext="&decreaseFontSizeToolbarCmd.tooltip;"
+ observes="cmd_decreaseFontStep"/>
+
+ <toolbarbutton id="IncreaseFontSizeButton"
+ class="formatting-button"
+ tooltiptext="&increaseFontSizeToolbarCmd.tooltip;"
+ observes="cmd_increaseFontStep"/>
+
+ <toolbarseparator class="toolbarseparator-standard"/>
+
+ <toolbarbutton id="boldButton"
+ class="formatting-button"
+ tooltiptext="&boldToolbarCmd.tooltip;"
+ type="checkbox"
+ autoCheck="false"
+ observes="cmd_bold"/>
+
+ <toolbarbutton id="italicButton"
+ class="formatting-button"
+ tooltiptext="&italicToolbarCmd.tooltip;"
+ type="checkbox"
+ autoCheck="false"
+ observes="cmd_italic"/>
+
+ <toolbarbutton id="underlineButton"
+ class="formatting-button"
+ tooltiptext="&underlineToolbarCmd.tooltip;"
+ type="checkbox"
+ autoCheck="false"
+ observes="cmd_underline"/>
+
+ <toolbarseparator class="toolbarseparator-standard"/>
+
+ <toolbarbutton id="removeStylingButton"
+ class="formatting-button"
+ data-l10n-id="compose-tool-button-remove-text-styling"
+ observes="cmd_removeStyles"/>
+
+ <toolbarseparator class="toolbarseparator-standard"/>
+
+ <toolbarbutton id="ulButton"
+ class="formatting-button"
+ tooltiptext="&bulletListToolbarCmd.tooltip;"
+ type="radio"
+ group="lists"
+ autoCheck="false"
+ observes="cmd_ul"/>
+
+ <toolbarbutton id="olButton"
+ class="formatting-button"
+ tooltiptext="&numberListToolbarCmd.tooltip;"
+ type="radio"
+ group="lists"
+ autoCheck="false"
+ observes="cmd_ol"/>
+
+ <toolbarbutton id="outdentButton"
+ class="formatting-button"
+ tooltiptext="&outdentToolbarCmd.tooltip;"
+ observes="cmd_outdent"/>
+
+ <toolbarbutton id="indentButton"
+ class="formatting-button"
+ tooltiptext="&indentToolbarCmd.tooltip;"
+ observes="cmd_indent"/>
+
+ <toolbarseparator class="toolbarseparator-standard"/>
+
+ <toolbarbutton id="AlignPopupButton"
+ type="menu"
+ wantdropmarker="true"
+ class="formatting-button"
+ tooltiptext="&AlignPopupButton.tooltip;"
+ observes="cmd_align">
+ <menupopup id="AlignPopup">
+ <menuitem id="AlignLeftItem" class="menuitem-iconic" label="&alignLeft.label;"
+ oncommand="doStatefulCommand('cmd_align', 'left')"
+ tooltiptext="&alignLeftButton.tooltip;" />
+ <menuitem id="AlignCenterItem" class="menuitem-iconic" label="&alignCenter.label;"
+ oncommand="doStatefulCommand('cmd_align', 'center')"
+ tooltiptext="&alignCenterButton.tooltip;" />
+ <menuitem id="AlignRightItem" class="menuitem-iconic" label="&alignRight.label;"
+ oncommand="doStatefulCommand('cmd_align', 'right')"
+ tooltiptext="&alignRightButton.tooltip;" />
+ <menuitem id="AlignJustifyItem" class="menuitem-iconic" label="&alignJustify.label;"
+ oncommand="doStatefulCommand('cmd_align', 'justify')"
+ tooltiptext="&alignJustifyButton.tooltip;" />
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- InsertPopupButton is used by messengercompose.xhtml -->
+ <toolbarbutton id="InsertPopupButton"
+ type="menu"
+ wantdropmarker="true"
+ class="formatting-button"
+ tooltiptext="&InsertPopupButton.tooltip;"
+ observes="cmd_renderedHTMLEnabler">
+ <menupopup id="InsertPopup">
+ <menuitem id="InsertLinkItem" class="menuitem-iconic" observes="cmd_link"
+ oncommand="goDoCommand('cmd_link')" label="&linkToolbarCmd.label;"
+ tooltiptext="&linkToolbarCmd.tooltip;" />
+ <menuitem id="InsertAnchorItem" class="menuitem-iconic" observes="cmd_anchor"
+ oncommand="goDoCommand('cmd_anchor')" label="&anchorToolbarCmd.label;"
+ tooltiptext="&anchorToolbarCmd.tooltip;" />
+ <menuitem id="InsertImageItem" class="menuitem-iconic" observes="cmd_image"
+ oncommand="goDoCommand('cmd_image')" label="&imageToolbarCmd.label;"
+ tooltiptext="&imageToolbarCmd.tooltip;" />
+ <menuitem id="InsertHRuleItem" class="menuitem-iconic" observes="cmd_hline"
+ oncommand="goDoCommand('cmd_hline')" label="&hruleToolbarCmd.label;"
+ tooltiptext="&hruleToolbarCmd.tooltip;" />
+ <menuitem id="InsertTableItem" class="menuitem-iconic" observes="cmd_table"
+ oncommand="goDoCommand('cmd_table')" label="&tableToolbarCmd.label;"
+ tooltiptext="&tableToolbarCmd.tooltip;" />
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarbutton id="smileButtonMenu"
+ type="menu"
+ wantdropmarker="true"
+ class="formatting-button"
+ tooltiptext="&SmileButton.tooltip;"
+ observes="cmd_smiley">
+ <menupopup id="smileyPopup" class="no-icon-menupopup">
+ <menuitem id="smileySmile" class="menuitem-iconic"
+ label="&#128578; &smiley1Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128578;')"/>
+ <menuitem id="smileyFrown" class="menuitem-iconic"
+ label="&#128577; &smiley2Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128577;')"/>
+ <menuitem id="smileyWink" class="menuitem-iconic"
+ label="&#128521; &smiley3Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128521;')"/>
+ <menuitem id="smileyTongue" class="menuitem-iconic"
+ label="&#128539; &smiley4Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128539;')"/>
+ <menuitem id="smileyLaughing" class="menuitem-iconic"
+ label="&#128514; &smiley5Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128514;')"/>
+ <menuitem id="smileyEmbarassed" class="menuitem-iconic"
+ label="&#128563; &smiley6Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128563;')"/>
+ <menuitem id="smileyUndecided" class="menuitem-iconic"
+ label="&#128533; &smiley7Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128533;')"/>
+ <menuitem id="smileySurprise" class="menuitem-iconic"
+ label="&#128558; &smiley8Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128558;')"/>
+ <menuitem id="smileyKiss" class="menuitem-iconic"
+ label="&#128536; &smiley9Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128536;')"/>
+ <menuitem id="smileyYell" class="menuitem-iconic"
+ label="&#128544; &smiley10Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128544;')"/>
+ <menuitem id="smileyCool" class="menuitem-iconic"
+ label="&#128526; &smiley11Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128526;')"/>
+ <menuitem id="smileyMoney" class="menuitem-iconic"
+ label="&#129297; &smiley12Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#129297;')"/>
+ <menuitem id="smileyFoot" class="menuitem-iconic"
+ label="&#128556; &smiley13Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128556;')"/>
+ <menuitem id="smileyInnocent" class="menuitem-iconic"
+ label="&#128519; &smiley14Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128519;')"/>
+ <menuitem id="smileyCry" class="menuitem-iconic"
+ label="&#128557; &smiley15Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128557;')"/>
+ <menuitem id="smileySealed" class="menuitem-iconic"
+ label="&#129296; &smiley16Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#129296;')"/>
+ </menupopup>
+ </toolbarbutton>
diff --git a/comm/mail/components/compose/content/editor.js b/comm/mail/components/compose/content/editor.js
new file mode 100644
index 0000000000..7535ecb0d1
--- /dev/null
+++ b/comm/mail/components/compose/content/editor.js
@@ -0,0 +1,2392 @@
+/* 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 ../../../../../toolkit/content/viewZoomOverlay.js */
+/* import-globals-from ../../../base/content/globalOverlay.js */
+/* import-globals-from ComposerCommands.js */
+/* import-globals-from editorUtilities.js */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/* Main Composer window UI control */
+
+var gComposerWindowControllerID = 0;
+var prefAuthorString = "";
+
+var kDisplayModeNormal = 0;
+var kDisplayModeAllTags = 1;
+var kDisplayModeSource = 2;
+var kDisplayModePreview = 3;
+
+const kDisplayModeMenuIDs = [
+ "viewNormalMode",
+ "viewAllTagsMode",
+ "viewSourceMode",
+ "viewPreviewMode",
+];
+const kDisplayModeTabIDS = [
+ "NormalModeButton",
+ "TagModeButton",
+ "SourceModeButton",
+ "PreviewModeButton",
+];
+const kNormalStyleSheet = "chrome://messenger/skin/shared/editorContent.css";
+const kContentEditableStyleSheet = "resource://gre/res/contenteditable.css";
+
+var kTextMimeType = "text/plain";
+var kHTMLMimeType = "text/html";
+var kXHTMLMimeType = "application/xhtml+xml";
+
+var gPreviousNonSourceDisplayMode = 1;
+var gEditorDisplayMode = -1;
+var gDocWasModified = false; // Check if clean document, if clean then unload when user "Opens"
+var gContentWindow = 0;
+var gSourceContentWindow = 0;
+var gSourceTextEditor = null;
+var gContentWindowDeck;
+var gFormatToolbar;
+var gFormatToolbarHidden = false;
+var gViewFormatToolbar;
+var gChromeState;
+var gColorObj = {
+ LastTextColor: "",
+ LastBackgroundColor: "",
+ LastHighlightColor: "",
+ Type: "",
+ SelectedType: "",
+ NoDefault: false,
+ Cancel: false,
+ HighlightColor: "",
+ BackgroundColor: "",
+ PageColor: "",
+ TextColor: "",
+ TableColor: "",
+ CellColor: "",
+};
+var gDefaultTextColor = "";
+var gDefaultBackgroundColor = "";
+var gCSSPrefListener;
+var gEditorToolbarPrefListener;
+var gReturnInParagraphPrefListener;
+var gLocalFonts = null;
+
+var gLastFocusNode = null;
+var gLastFocusNodeWasSelected = false;
+
+// These must be kept in synch with the XUL <options> lists
+var gFontSizeNames = [
+ "xx-small",
+ "x-small",
+ "small",
+ "medium",
+ "large",
+ "x-large",
+ "xx-large",
+];
+
+var kUseCssPref = "editor.use_css";
+var kCRInParagraphsPref = "editor.CR_creates_new_p";
+
+// This should be called by all editor users when they close their window.
+function EditorCleanup() {
+ SwitchInsertCharToAnotherEditorOrClose();
+}
+
+/** @implements {nsIDocumentStateListener} */
+var DocumentReloadListener = {
+ NotifyDocumentWillBeDestroyed() {},
+
+ NotifyDocumentStateChanged(isNowDirty) {
+ var editor = GetCurrentEditor();
+ try {
+ // unregister the listener to prevent multiple callbacks
+ editor.removeDocumentStateListener(DocumentReloadListener);
+
+ var charset = editor.documentCharacterSet;
+
+ // update the META charset with the current presentation charset
+ editor.documentCharacterSet = charset;
+ } catch (e) {}
+ },
+};
+
+// implements nsIObserver
+var gEditorDocumentObserver = {
+ observe(aSubject, aTopic, aData) {
+ // Should we allow this even if NOT the focused editor?
+ var commandManager = GetCurrentCommandManager();
+ if (commandManager != aSubject) {
+ return;
+ }
+
+ var editor = GetCurrentEditor();
+ switch (aTopic) {
+ case "obs_documentCreated":
+ // Just for convenience
+ gContentWindow = window.content;
+
+ // Get state to see if document creation succeeded
+ var params = newCommandParams();
+ if (!params) {
+ return;
+ }
+
+ try {
+ commandManager.getCommandState(aTopic, gContentWindow, params);
+ var errorStringId = 0;
+ var editorStatus = params.getLongValue("state_data");
+ if (!editor && editorStatus == Ci.nsIEditingSession.eEditorOK) {
+ dump(
+ "\n ****** NO EDITOR BUT NO EDITOR ERROR REPORTED ******* \n\n"
+ );
+ editorStatus = Ci.nsIEditingSession.eEditorErrorUnknown;
+ }
+
+ switch (editorStatus) {
+ case Ci.nsIEditingSession.eEditorErrorCantEditFramesets:
+ errorStringId = "CantEditFramesetMsg";
+ break;
+ case Ci.nsIEditingSession.eEditorErrorCantEditMimeType:
+ errorStringId = "CantEditMimeTypeMsg";
+ break;
+ case Ci.nsIEditingSession.eEditorErrorUnknown:
+ errorStringId = "CantEditDocumentMsg";
+ break;
+ // Note that for "eEditorErrorFileNotFound,
+ // network code popped up an alert dialog, so we don't need to
+ }
+ if (errorStringId) {
+ Services.prompt.alert(window, "", GetString(errorStringId));
+ }
+ } catch (e) {
+ dump("EXCEPTION GETTING obs_documentCreated state " + e + "\n");
+ }
+
+ // We have a bad editor -- nsIEditingSession will rebuild an editor
+ // with a blank page, so simply abort here
+ if (editorStatus) {
+ return;
+ }
+
+ if (!("InsertCharWindow" in window)) {
+ window.InsertCharWindow = null;
+ }
+
+ let domWindowUtils =
+ GetCurrentEditorElement().contentWindow.windowUtils;
+ // And extra styles for showing anchors, table borders, smileys, etc.
+ domWindowUtils.loadSheetUsingURIString(
+ kNormalStyleSheet,
+ domWindowUtils.AGENT_SHEET
+ );
+
+ // Remove contenteditable stylesheets if they were applied by the
+ // editingSession.
+ domWindowUtils.removeSheetUsingURIString(
+ kContentEditableStyleSheet,
+ domWindowUtils.AGENT_SHEET
+ );
+
+ // Add mouse click watcher if right type of editor
+ if (IsHTMLEditor()) {
+ // Force color widgets to update
+ onFontColorChange();
+ onBackgroundColorChange();
+ }
+ break;
+
+ case "cmd_setDocumentModified":
+ window.updateCommands("save");
+ break;
+
+ case "obs_documentWillBeDestroyed":
+ dump("obs_documentWillBeDestroyed notification\n");
+ break;
+
+ case "obs_documentLocationChanged":
+ // Ignore this when editor doesn't exist,
+ // which happens once when page load starts
+ if (editor) {
+ try {
+ editor.updateBaseURL();
+ } catch (e) {
+ dump(e);
+ }
+ }
+ break;
+
+ case "cmd_bold":
+ // Update all style items
+ // cmd_bold is a proxy; see EditorSharedStartup (above) for details
+ window.updateCommands("style");
+ window.updateCommands("undo");
+ break;
+ }
+ },
+};
+
+function SetFocusOnStartup() {
+ gContentWindow.focus();
+}
+
+function EditorLoadUrl(url) {
+ try {
+ if (url) {
+ let loadURIOptions = {
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ GetCurrentEditorElement().webNavigation.fixupAndLoadURIString(
+ url,
+ loadURIOptions
+ );
+ }
+ } catch (e) {
+ dump(" EditorLoadUrl failed: " + e + "\n");
+ }
+}
+
+// This should be called by all Composer types
+function EditorSharedStartup() {
+ // Just for convenience
+ gContentWindow = window.content;
+
+ // Disable DNS Prefetching on the docshell - we don't need it for composer
+ // type windows.
+ GetCurrentEditorElement().docShell.allowDNSPrefetch = false;
+
+ let messageEditorBrowser = GetCurrentEditorElement();
+ messageEditorBrowser.addEventListener(
+ "DoZoomEnlargeBy10",
+ () => {
+ ZoomManager.scrollZoomEnlarge(messageEditorBrowser);
+ },
+ true
+ );
+ messageEditorBrowser.addEventListener(
+ "DoZoomReduceBy10",
+ () => {
+ ZoomManager.scrollReduceEnlarge(messageEditorBrowser);
+ },
+ true
+ );
+
+ // Set up the mime type and register the commands.
+ if (IsHTMLEditor()) {
+ SetupHTMLEditorCommands();
+ } else {
+ SetupTextEditorCommands();
+ }
+
+ // add observer to be called when document is really done loading
+ // and is modified
+ // Note: We're really screwed if we fail to install this observer!
+ try {
+ var commandManager = GetCurrentCommandManager();
+ commandManager.addCommandObserver(
+ gEditorDocumentObserver,
+ "obs_documentCreated"
+ );
+ commandManager.addCommandObserver(
+ gEditorDocumentObserver,
+ "cmd_setDocumentModified"
+ );
+ commandManager.addCommandObserver(
+ gEditorDocumentObserver,
+ "obs_documentWillBeDestroyed"
+ );
+ commandManager.addCommandObserver(
+ gEditorDocumentObserver,
+ "obs_documentLocationChanged"
+ );
+
+ // Until nsIControllerCommandGroup-based code is implemented,
+ // we will observe just the bold command to trigger update of
+ // all toolbar style items
+ commandManager.addCommandObserver(gEditorDocumentObserver, "cmd_bold");
+ } catch (e) {
+ dump(e);
+ }
+
+ var isMac = AppConstants.platform == "macosx";
+
+ // Set platform-specific hints for how to select cells
+ // Mac uses "Cmd", all others use "Ctrl"
+ var tableKey = GetString(isMac ? "XulKeyMac" : "TableSelectKey");
+ var dragStr = tableKey + GetString("Drag");
+ var clickStr = tableKey + GetString("Click");
+
+ var delStr = GetString(isMac ? "Clear" : "Del");
+
+ SafeSetAttribute("menu_SelectCell", "acceltext", clickStr);
+ SafeSetAttribute("menu_SelectRow", "acceltext", dragStr);
+ SafeSetAttribute("menu_SelectColumn", "acceltext", dragStr);
+ SafeSetAttribute("menu_SelectAllCells", "acceltext", dragStr);
+ // And add "Del" or "Clear"
+ SafeSetAttribute("menu_DeleteCellContents", "acceltext", delStr);
+
+ // Set text for indent, outdent keybinding
+
+ // hide UI that we don't have components for
+ RemoveInapplicableUIElements();
+
+ // Use browser colors as initial values for editor's default colors
+ var BrowserColors = GetDefaultBrowserColors();
+ if (BrowserColors) {
+ gDefaultTextColor = BrowserColors.TextColor;
+ gDefaultBackgroundColor = BrowserColors.BackgroundColor;
+ }
+
+ // For new window, no default last-picked colors
+ gColorObj.LastTextColor = "";
+ gColorObj.LastBackgroundColor = "";
+ gColorObj.LastHighlightColor = "";
+}
+
+function SafeSetAttribute(nodeID, attributeName, attributeValue) {
+ var theNode = document.getElementById(nodeID);
+ if (theNode) {
+ theNode.setAttribute(attributeName, attributeValue);
+ }
+}
+
+async function CheckAndSaveDocument(command, allowDontSave) {
+ var document;
+ try {
+ // if we don't have an editor or an document, bail
+ var editor = GetCurrentEditor();
+ document = editor.document;
+ if (!document) {
+ return true;
+ }
+ } catch (e) {
+ return true;
+ }
+
+ if (!IsDocumentModified() && !IsHTMLSourceChanged()) {
+ return true;
+ }
+
+ // call window.focus, since we need to pop up a dialog
+ // and therefore need to be visible (to prevent user confusion)
+ top.document.commandDispatcher.focusedWindow.focus();
+
+ var strID;
+ switch (command) {
+ case "cmd_close":
+ strID = "BeforeClosing";
+ break;
+ }
+
+ var reasonToSave = strID ? GetString(strID) : "";
+
+ var title = document.title || GetString("untitledDefaultFilename");
+
+ var dialogTitle = GetString("SaveDocument");
+ var dialogMsg = GetString("SaveFilePrompt");
+ dialogMsg = dialogMsg
+ .replace(/%title%/, title)
+ .replace(/%reason%/, reasonToSave);
+
+ let result = { value: 0 };
+ let promptFlags =
+ Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;
+ let button1Title = null;
+ let button3Title = null;
+
+ promptFlags +=
+ Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0;
+
+ // If allowing "Don't..." button, add that
+ if (allowDontSave) {
+ promptFlags +=
+ Services.prompt.BUTTON_TITLE_DONT_SAVE * Services.prompt.BUTTON_POS_2;
+ }
+
+ result = Services.prompt.confirmEx(
+ window,
+ dialogTitle,
+ dialogMsg,
+ promptFlags,
+ button1Title,
+ null,
+ button3Title,
+ null,
+ { value: 0 }
+ );
+
+ if (result == 0) {
+ // Save to local disk
+ return SaveDocument(false, false, editor.contentsMIMEType);
+ }
+
+ if (result == 2) {
+ // "Don't Save"
+ return true;
+ }
+
+ // Default or result == 1 (Cancel)
+ return false;
+}
+
+// --------------------------- Text style ---------------------------
+
+function editorSetParagraphState(state) {
+ if (state === "") {
+ // Corresponds to body text. Has no corresponding formatBlock value.
+ goDoCommandParams("cmd_paragraphState", "");
+ } else {
+ GetCurrentEditor().document.execCommand("formatBlock", false, state);
+ }
+ document.getElementById("cmd_paragraphState").setAttribute("state", state);
+ onParagraphFormatChange();
+}
+
+function onParagraphFormatChange() {
+ let paraMenuList = document.getElementById("ParagraphSelect");
+ if (!paraMenuList) {
+ return;
+ }
+
+ var commandNode = document.getElementById("cmd_paragraphState");
+ var state = commandNode.getAttribute("state");
+
+ // force match with "normal"
+ if (state == "body") {
+ state = "";
+ }
+
+ if (state == "mixed") {
+ // Selection is the "mixed" ( > 1 style) state
+ paraMenuList.selectedItem = null;
+ paraMenuList.setAttribute("label", GetString("Mixed"));
+ } else {
+ var menuPopup = document.getElementById("ParagraphPopup");
+ for (let menuItem of menuPopup.children) {
+ if (menuItem.value === state) {
+ paraMenuList.selectedItem = menuItem;
+ break;
+ }
+ }
+ }
+}
+
+function editorRemoveTextStyling() {
+ GetCurrentEditor().document.execCommand("removeFormat", false, null);
+ // After removing the formatting, update the full styling command set.
+ window.updateCommands("style");
+}
+
+/**
+ * Selects the current font face in the menulist.
+ */
+function onFontFaceChange() {
+ let fontFaceMenuList = document.getElementById("FontFaceSelect");
+ var commandNode = document.getElementById("cmd_fontFace");
+ var editorFont = commandNode.getAttribute("state");
+
+ // Strip quotes in font names. Experiments have shown that we only
+ // ever get double quotes around the font name, never single quotes,
+ // even if they were in the HTML source. Also single or double
+ // quotes within the font name are never returned.
+ editorFont = editorFont.replace(/"/g, "");
+
+ switch (editorFont) {
+ case "mixed":
+ // Selection is the "mixed" ( > 1 style) state.
+ fontFaceMenuList.selectedItem = null;
+ fontFaceMenuList.setAttribute("label", GetString("Mixed"));
+ return;
+ case "":
+ case "serif":
+ case "sans-serif":
+ // Generic variable width.
+ fontFaceMenuList.selectedIndex = 0;
+ return;
+ case "tt":
+ case "monospace":
+ // Generic fixed width.
+ fontFaceMenuList.selectedIndex = 1;
+ return;
+ default:
+ }
+
+ let menuPopup = fontFaceMenuList.menupopup;
+ let menuItems = menuPopup.children;
+
+ const genericFamilies = [
+ "serif",
+ "sans-serif",
+ "monospace",
+ "fantasy",
+ "cursive",
+ ];
+ // Bug 1139524: Normalise before we compare: Make it lower case
+ // and replace ", " with "," so that entries like
+ // "Helvetica, Arial, sans-serif" are always recognised correctly
+ let editorFontToLower = editorFont.toLowerCase().replace(/, /g, ",");
+ let foundFont = null;
+ let exactMatch = false;
+ let usedFontsSep = menuPopup.querySelector(
+ "menuseparator.fontFaceMenuAfterUsedFonts"
+ );
+ let editorFontOptions = editorFontToLower.split(",");
+ let editorOptionsCount = editorFontOptions.length;
+ let matchedFontIndex = editorOptionsCount; // initialise to high invalid value
+
+ // The font menu has this structure:
+ // 0: Variable Width
+ // 1: Fixed Width
+ // 2: Separator
+ // 3: Helvetica, Arial (stored as Helvetica, Arial, sans-serif)
+ // 4: Times (stored as Times New Roman, Times, serif)
+ // 5: Courier (stored as Courier New, Courier, monospace)
+ // 6: Separator, "menuseparator.fontFaceMenuAfterDefaultFonts"
+ // from 7: Used Font Section (for quick selection)
+ // followed by separator, "menuseparator.fontFaceMenuAfterUsedFonts"
+ // followed by all other available fonts.
+ // The following variable keeps track of where we are when we loop over the menu.
+ let afterUsedFontSection = false;
+
+ // The menu items not only have "label" and "value", but also some other attributes:
+ // "value_parsed": Is the toLowerCase() and space-stripped value.
+ // "value_cache": Is a concatenation of all editor fonts that were ever mapped
+ // onto this menu item. This is done for optimization.
+ // "used": This item is in the used font section.
+
+ for (let i = 0; i < menuItems.length; i++) {
+ let menuItem = menuItems.item(i);
+ if (
+ menuItem.hasAttribute("label") &&
+ menuItem.hasAttribute("value_parsed")
+ ) {
+ // The element seems to represent a font <menuitem>.
+ let fontMenuValue = menuItem.getAttribute("value_parsed");
+ if (
+ fontMenuValue == editorFontToLower ||
+ (menuItem.hasAttribute("value_cache") &&
+ menuItem
+ .getAttribute("value_cache")
+ .split("|")
+ .includes(editorFontToLower))
+ ) {
+ // This menuitem contains the font we are looking for.
+ foundFont = menuItem;
+ exactMatch = true;
+ break;
+ } else if (editorOptionsCount > 1 && afterUsedFontSection) {
+ // Once we are in the list of all other available fonts,
+ // we will find the one that best matches one of the options.
+ let matchPos = editorFontOptions.indexOf(fontMenuValue);
+ if (matchPos >= 0 && matchPos < matchedFontIndex) {
+ // This menu font comes earlier in the list of options,
+ // so prefer it.
+ matchedFontIndex = matchPos;
+ foundFont = menuItem;
+ // If we matched the first option, we don't need to look for
+ // a better match.
+ if (matchPos == 0) {
+ break;
+ }
+ }
+ }
+ } else if (menuItem == usedFontsSep) {
+ // Some other element type.
+ // We have now passed the section of used fonts and are now in the list of all.
+ afterUsedFontSection = true;
+ }
+ }
+
+ if (foundFont) {
+ let defaultFontsSep = menuPopup.querySelector(
+ "menuseparator.fontFaceMenuAfterDefaultFonts"
+ );
+ if (exactMatch) {
+ if (afterUsedFontSection) {
+ // Copy the matched font into the section of used fonts.
+ // We insert after the separator following the default fonts,
+ // so right at the beginning of the used fonts section.
+ let copyItem = foundFont.cloneNode(true);
+ menuPopup.insertBefore(copyItem, defaultFontsSep.nextElementSibling);
+ usedFontsSep.hidden = false;
+ foundFont = copyItem;
+ foundFont.setAttribute("used", "true");
+ }
+ } else {
+ // Keep only the found font and generic families in the font string.
+ editorFont = editorFont
+ .replace(/, /g, ",")
+ .split(",")
+ .filter(
+ font =>
+ font.toLowerCase() == foundFont.getAttribute("value_parsed") ||
+ genericFamilies.includes(font)
+ )
+ .join(",");
+
+ // Check if such an item is already in the used font section.
+ if (afterUsedFontSection) {
+ foundFont = menuPopup.querySelector(
+ 'menuitem[used="true"][value_parsed="' +
+ editorFont.toLowerCase() +
+ '"]'
+ );
+ }
+ // If not, create a new entry which will be inserted into that section.
+ if (!foundFont) {
+ foundFont = createFontFaceMenuitem(editorFont, editorFont, menuPopup);
+ }
+
+ // Add the editor font string into the 'cache' attribute in the element
+ // so we can later find it quickly without building the reduced string again.
+ let fontCache = "";
+ if (foundFont.hasAttribute("value_cache")) {
+ fontCache = foundFont.getAttribute("value_cache");
+ }
+ foundFont.setAttribute(
+ "value_cache",
+ fontCache + "|" + editorFontToLower
+ );
+
+ // If we created a new item, set it up and insert.
+ if (!foundFont.hasAttribute("used")) {
+ foundFont.setAttribute("used", "true");
+ usedFontsSep.hidden = false;
+ menuPopup.insertBefore(foundFont, defaultFontsSep.nextElementSibling);
+ }
+ }
+ } else {
+ // The editor encountered a font that is not installed on this system.
+ // Add it to the font menu now, in the used-fonts section right at the
+ // bottom before the separator of the section.
+ let fontLabel = GetFormattedString("NotInstalled", editorFont);
+ foundFont = createFontFaceMenuitem(fontLabel, editorFont, menuPopup);
+ foundFont.setAttribute("used", "true");
+ usedFontsSep.hidden = false;
+ menuPopup.insertBefore(foundFont, usedFontsSep);
+ }
+ fontFaceMenuList.selectedItem = foundFont;
+}
+
+/**
+ * Changes the font size for the selection or at the insertion point. This
+ * requires an integer from 1-7 as a value argument (x-small - xxx-large)
+ *
+ * @param {"1"|"2"|"3"|"4"|"5"|"6"|"7"} size - The font size.
+ */
+function EditorSetFontSize(size) {
+ // For normal/medium size (that is 3), we clear size.
+ if (size == "3") {
+ EditorRemoveTextProperty("font", "size");
+ // Also remove big and small,
+ // else it will seem like size isn't changing correctly
+ EditorRemoveTextProperty("small", "");
+ EditorRemoveTextProperty("big", "");
+ } else {
+ GetCurrentEditor().document.execCommand("fontSize", false, size);
+ }
+ // Enable or Disable the toolbar buttons according to the font size.
+ goUpdateCommand("cmd_decreaseFontStep");
+ goUpdateCommand("cmd_increaseFontStep");
+ gContentWindow.focus();
+}
+
+function initFontFaceMenu(menuPopup) {
+ initLocalFontFaceMenu(menuPopup);
+
+ if (menuPopup) {
+ var children = menuPopup.children;
+ if (!children) {
+ return;
+ }
+
+ var mixed = { value: false };
+ var editorFont = GetCurrentEditor().getFontFaceState(mixed);
+
+ // Strip quotes in font names. Experiments have shown that we only
+ // ever get double quotes around the font name, never single quotes,
+ // even if they were in the HTML source. Also single or double
+ // quotes within the font name are never returned.
+ editorFont = editorFont.replace(/"/g, "");
+
+ if (!mixed.value) {
+ switch (editorFont) {
+ case "":
+ case "serif":
+ case "sans-serif":
+ // Generic variable width.
+ editorFont = "";
+ break;
+ case "tt":
+ case "monospace":
+ // Generic fixed width.
+ editorFont = "monospace";
+ break;
+ default:
+ editorFont = editorFont.toLowerCase().replace(/, /g, ","); // bug 1139524
+ }
+ }
+
+ var editorFontOptions = editorFont.split(",");
+ var matchedOption = editorFontOptions.length; // initialise to high invalid value
+ for (var i = 0; i < children.length; i++) {
+ var menuItem = children[i];
+ if (menuItem.localName == "menuitem") {
+ var matchFound = false;
+ if (!mixed.value) {
+ var menuFont = menuItem
+ .getAttribute("value")
+ .toLowerCase()
+ .replace(/, /g, ",");
+
+ // First compare the entire font string to match items that contain commas.
+ if (menuFont == editorFont) {
+ menuItem.setAttribute("checked", "true");
+ break;
+ } else if (editorFontOptions.length > 1) {
+ // Next compare the individual options.
+ var matchPos = editorFontOptions.indexOf(menuFont);
+ if (matchPos >= 0 && matchPos < matchedOption) {
+ // This menu font comes earlier in the list of options,
+ // so prefer it.
+ menuItem.setAttribute("checked", "true");
+
+ // If we matched the first option, we don't need to look for
+ // a better match.
+ if (matchPos == 0) {
+ break;
+ }
+
+ matchedOption = matchPos;
+ matchFound = true;
+ }
+ }
+ }
+
+ // In case this item doesn't match, make sure we've cleared the checkmark.
+ if (!matchFound) {
+ menuItem.removeAttribute("checked");
+ }
+ }
+ }
+ }
+}
+
+// Number of fixed font face menuitems, these are:
+// Variable Width
+// Fixed Width
+// ==separator
+// Helvetica, Arial
+// Times
+// Courier
+// ==separator
+// ==separator
+const kFixedFontFaceMenuItems = 8;
+
+function initLocalFontFaceMenu(menuPopup) {
+ if (!gLocalFonts) {
+ // Build list of all local fonts once per editor
+ try {
+ var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"].getService(
+ Ci.nsIFontEnumerator
+ );
+ gLocalFonts = enumerator.EnumerateAllFonts();
+ } catch (e) {}
+ }
+
+ // Don't use radios for menulists.
+ let useRadioMenuitems = menuPopup.parentNode.localName == "menu";
+ menuPopup.setAttribute("useRadios", useRadioMenuitems);
+ if (menuPopup.children.length == kFixedFontFaceMenuItems) {
+ if (gLocalFonts.length == 0) {
+ menuPopup.querySelector(".fontFaceMenuAfterDefaultFonts").hidden = true;
+ }
+ for (let i = 0; i < gLocalFonts.length; ++i) {
+ // Remove Linux system generic fonts that collide with CSS generic fonts.
+ if (
+ gLocalFonts[i] != "" &&
+ gLocalFonts[i] != "serif" &&
+ gLocalFonts[i] != "sans-serif" &&
+ gLocalFonts[i] != "monospace"
+ ) {
+ let itemNode = createFontFaceMenuitem(
+ gLocalFonts[i],
+ gLocalFonts[i],
+ menuPopup
+ );
+ menuPopup.appendChild(itemNode);
+ }
+ }
+ }
+}
+
+/**
+ * Creates a menuitem element for the font faces menulist. Returns the menuitem
+ * but does not add it automatically to the menupopup.
+ *
+ * @param aFontLabel Label to be displayed for the item.
+ * @param aFontName The font face value to be used for the item.
+ * Will be used in <font face="value"> in the edited document.
+ * @param aMenuPopup The menupopup for which this menuitem is created.
+ */
+function createFontFaceMenuitem(aFontLabel, aFontName, aMenuPopup) {
+ let itemNode = document.createXULElement("menuitem");
+ itemNode.setAttribute("label", aFontLabel);
+ itemNode.setAttribute("value", aFontName);
+ itemNode.setAttribute(
+ "value_parsed",
+ aFontName.toLowerCase().replace(/, /g, ",")
+ );
+ itemNode.setAttribute("tooltiptext", aFontLabel);
+ if (aMenuPopup.getAttribute("useRadios") == "true") {
+ itemNode.setAttribute("type", "radio");
+ itemNode.setAttribute("observes", "cmd_renderedHTMLEnabler");
+ }
+ return itemNode;
+}
+
+/**
+ * Helper function
+ *
+ * @see https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#legacy-font-size-for
+ */
+function getLegacyFontSize() {
+ let fontSize = GetCurrentEditor().document.queryCommandValue("fontSize");
+ // If one selects all the texts in the editor and deletes it, the editor
+ // will return null fontSize. We will set it to default value then.
+ if (!fontSize) {
+ fontSize = Services.prefs.getCharPref("msgcompose.font_size", "3");
+ }
+ return fontSize;
+}
+
+function initFontSizeMenu(menuPopup) {
+ if (menuPopup) {
+ let fontSize = getLegacyFontSize();
+ for (let menuitem of menuPopup.children) {
+ if (menuitem.getAttribute("value") == fontSize) {
+ menuitem.setAttribute("checked", true);
+ }
+ }
+ }
+}
+
+function onFontColorChange() {
+ ChangeButtonColor("cmd_fontColor", "TextColorButton", gDefaultTextColor);
+}
+
+function onBackgroundColorChange() {
+ ChangeButtonColor(
+ "cmd_backgroundColor",
+ "BackgroundColorButton",
+ gDefaultBackgroundColor
+ );
+}
+
+/* Helper function that changes the button color.
+ * commandID - The ID of the command element.
+ * id - The ID of the button needing to be changed.
+ * defaultColor - The default color the button gets set to.
+ */
+function ChangeButtonColor(commandID, id, defaultColor) {
+ var commandNode = document.getElementById(commandID);
+ if (commandNode) {
+ var color = commandNode.getAttribute("state");
+ var button = document.getElementById(id);
+ if (button) {
+ button.setAttribute("color", color);
+
+ // No color or a mixed color - get color set on page or other defaults.
+ if (!color || color == "mixed") {
+ color = defaultColor;
+ }
+
+ button.style.backgroundColor = color;
+ }
+ }
+}
+
+// Call this when user changes text and/or background colors of the page
+function UpdateDefaultColors() {
+ var BrowserColors = GetDefaultBrowserColors();
+ var bodyelement = GetBodyElement();
+ var defTextColor = gDefaultTextColor;
+ var defBackColor = gDefaultBackgroundColor;
+
+ if (bodyelement) {
+ var color = bodyelement.getAttribute("text");
+ if (color) {
+ gDefaultTextColor = color;
+ } else if (BrowserColors) {
+ gDefaultTextColor = BrowserColors.TextColor;
+ }
+
+ color = bodyelement.getAttribute("bgcolor");
+ if (color) {
+ gDefaultBackgroundColor = color;
+ } else if (BrowserColors) {
+ gDefaultBackgroundColor = BrowserColors.BackgroundColor;
+ }
+ }
+
+ // Trigger update on toolbar
+ if (defTextColor != gDefaultTextColor) {
+ goUpdateCommandState("cmd_fontColor");
+ onFontColorChange();
+ }
+ if (defBackColor != gDefaultBackgroundColor) {
+ goUpdateCommandState("cmd_backgroundColor");
+ onBackgroundColorChange();
+ }
+}
+
+function GetBackgroundElementWithColor() {
+ var editor = GetCurrentTableEditor();
+ if (!editor) {
+ return null;
+ }
+
+ gColorObj.Type = "";
+ gColorObj.PageColor = "";
+ gColorObj.TableColor = "";
+ gColorObj.CellColor = "";
+ gColorObj.BackgroundColor = "";
+ gColorObj.SelectedType = "";
+
+ var tagNameObj = { value: "" };
+ var element;
+ try {
+ element = editor.getSelectedOrParentTableElement(tagNameObj, { value: 0 });
+ } catch (e) {}
+
+ if (element && tagNameObj && tagNameObj.value) {
+ gColorObj.BackgroundColor = GetHTMLOrCSSStyleValue(
+ element,
+ "bgcolor",
+ "background-color"
+ );
+ gColorObj.BackgroundColor = ConvertRGBColorIntoHEXColor(
+ gColorObj.BackgroundColor
+ );
+ if (tagNameObj.value.toLowerCase() == "td") {
+ gColorObj.Type = "Cell";
+ gColorObj.CellColor = gColorObj.BackgroundColor;
+
+ // Get any color that might be on parent table
+ var table = GetParentTable(element);
+ gColorObj.TableColor = GetHTMLOrCSSStyleValue(
+ table,
+ "bgcolor",
+ "background-color"
+ );
+ gColorObj.TableColor = ConvertRGBColorIntoHEXColor(gColorObj.TableColor);
+ } else {
+ gColorObj.Type = "Table";
+ gColorObj.TableColor = gColorObj.BackgroundColor;
+ }
+ gColorObj.SelectedType = gColorObj.Type;
+ } else {
+ let IsCSSPrefChecked = Services.prefs.getBoolPref(kUseCssPref);
+ if (IsCSSPrefChecked && IsHTMLEditor()) {
+ let selection = editor.selection;
+ if (selection) {
+ element = selection.focusNode;
+ while (!editor.nodeIsBlock(element)) {
+ element = element.parentNode;
+ }
+ } else {
+ element = GetBodyElement();
+ }
+ } else {
+ element = GetBodyElement();
+ }
+ if (element) {
+ gColorObj.Type = "Page";
+ gColorObj.BackgroundColor = GetHTMLOrCSSStyleValue(
+ element,
+ "bgcolor",
+ "background-color"
+ );
+ if (gColorObj.BackgroundColor == "") {
+ gColorObj.BackgroundColor = "transparent";
+ } else {
+ gColorObj.BackgroundColor = ConvertRGBColorIntoHEXColor(
+ gColorObj.BackgroundColor
+ );
+ }
+ gColorObj.PageColor = gColorObj.BackgroundColor;
+ }
+ }
+ return element;
+}
+
+/* eslint-disable complexity */
+function EditorSelectColor(colorType, mouseEvent) {
+ var editor = GetCurrentEditor();
+ if (!editor || !gColorObj) {
+ return;
+ }
+
+ // Shift + mouse click automatically applies last color, if available
+ var useLastColor = mouseEvent
+ ? mouseEvent.button == 0 && mouseEvent.shiftKey
+ : false;
+ var element;
+ var table;
+ var currentColor = "";
+ var commandNode;
+
+ if (!colorType) {
+ colorType = "";
+ }
+
+ if (colorType == "Text") {
+ gColorObj.Type = colorType;
+
+ // Get color from command node state
+ commandNode = document.getElementById("cmd_fontColor");
+ currentColor = commandNode.getAttribute("state");
+ currentColor = ConvertRGBColorIntoHEXColor(currentColor);
+ gColorObj.TextColor = currentColor;
+
+ if (useLastColor && gColorObj.LastTextColor) {
+ gColorObj.TextColor = gColorObj.LastTextColor;
+ } else {
+ useLastColor = false;
+ }
+ } else if (colorType == "Highlight") {
+ gColorObj.Type = colorType;
+
+ // Get color from command node state
+ commandNode = document.getElementById("cmd_highlight");
+ currentColor = commandNode.getAttribute("state");
+ currentColor = ConvertRGBColorIntoHEXColor(currentColor);
+ gColorObj.HighlightColor = currentColor;
+
+ if (useLastColor && gColorObj.LastHighlightColor) {
+ gColorObj.HighlightColor = gColorObj.LastHighlightColor;
+ } else {
+ useLastColor = false;
+ }
+ } else {
+ element = GetBackgroundElementWithColor();
+ if (!element) {
+ return;
+ }
+
+ // Get the table if we found a cell
+ if (gColorObj.Type == "Table") {
+ table = element;
+ } else if (gColorObj.Type == "Cell") {
+ table = GetParentTable(element);
+ }
+
+ // Save to avoid resetting if not necessary
+ currentColor = gColorObj.BackgroundColor;
+
+ if (colorType == "TableOrCell" || colorType == "Cell") {
+ if (gColorObj.Type == "Cell") {
+ gColorObj.Type = colorType;
+ } else if (gColorObj.Type != "Table") {
+ return;
+ }
+ } else if (colorType == "Table" && gColorObj.Type == "Page") {
+ return;
+ }
+
+ if (colorType == "" && gColorObj.Type == "Cell") {
+ // Using empty string for requested type means
+ // we can let user select cell or table
+ gColorObj.Type = "TableOrCell";
+ }
+
+ if (useLastColor && gColorObj.LastBackgroundColor) {
+ gColorObj.BackgroundColor = gColorObj.LastBackgroundColor;
+ } else {
+ useLastColor = false;
+ }
+ }
+ // Save the type we are really requesting
+ colorType = gColorObj.Type;
+
+ if (!useLastColor) {
+ // Avoid the JS warning
+ gColorObj.NoDefault = false;
+
+ // Launch the ColorPicker dialog
+ // TODO: Figure out how to position this under the color buttons on the toolbar
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdColorPicker.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ "",
+ gColorObj
+ );
+
+ // User canceled the dialog
+ if (gColorObj.Cancel) {
+ return;
+ }
+ }
+
+ if (gColorObj.Type == "Text") {
+ if (currentColor != gColorObj.TextColor) {
+ if (gColorObj.TextColor) {
+ GetCurrentEditor().document.execCommand(
+ "foreColor",
+ false,
+ gColorObj.TextColor
+ );
+ } else {
+ EditorRemoveTextProperty("font", "color");
+ }
+ }
+ // Update the command state (this will trigger color button update)
+ goUpdateCommandState("cmd_fontColor");
+ } else if (gColorObj.Type == "Highlight") {
+ if (currentColor != gColorObj.HighlightColor) {
+ if (gColorObj.HighlightColor) {
+ GetCurrentEditor().document.execCommand(
+ "backColor",
+ false,
+ gColorObj.HighlightColor
+ );
+ } else {
+ EditorRemoveTextProperty("font", "bgcolor");
+ }
+ }
+ // Update the command state (this will trigger color button update)
+ goUpdateCommandState("cmd_highlight");
+ } else if (element) {
+ if (gColorObj.Type == "Table") {
+ // Set background on a table
+ // Note that we shouldn't trust "currentColor" because of "TableOrCell" behavior
+ if (table) {
+ var bgcolor = table.getAttribute("bgcolor");
+ if (bgcolor != gColorObj.BackgroundColor) {
+ try {
+ if (gColorObj.BackgroundColor) {
+ editor.setAttributeOrEquivalent(
+ table,
+ "bgcolor",
+ gColorObj.BackgroundColor,
+ false
+ );
+ } else {
+ editor.removeAttributeOrEquivalent(table, "bgcolor", false);
+ }
+ } catch (e) {}
+ }
+ }
+ } else if (currentColor != gColorObj.BackgroundColor && IsHTMLEditor()) {
+ editor.beginTransaction();
+ try {
+ editor.setBackgroundColor(gColorObj.BackgroundColor);
+
+ if (gColorObj.Type == "Page" && gColorObj.BackgroundColor) {
+ // Set all page colors not explicitly set,
+ // else you can end up with unreadable pages
+ // because viewer's default colors may not be same as page author's
+ var bodyelement = GetBodyElement();
+ if (bodyelement) {
+ var defColors = GetDefaultBrowserColors();
+ if (defColors) {
+ if (!bodyelement.getAttribute("text")) {
+ editor.setAttributeOrEquivalent(
+ bodyelement,
+ "text",
+ defColors.TextColor,
+ false
+ );
+ }
+
+ // The following attributes have no individual CSS declaration counterparts
+ // Getting rid of them in favor of CSS implies CSS rules management
+ if (!bodyelement.getAttribute("link")) {
+ editor.setAttribute(bodyelement, "link", defColors.LinkColor);
+ }
+
+ if (!bodyelement.getAttribute("alink")) {
+ editor.setAttribute(
+ bodyelement,
+ "alink",
+ defColors.ActiveLinkColor
+ );
+ }
+
+ if (!bodyelement.getAttribute("vlink")) {
+ editor.setAttribute(
+ bodyelement,
+ "vlink",
+ defColors.VisitedLinkColor
+ );
+ }
+ }
+ }
+ }
+ } catch (e) {}
+
+ editor.endTransaction();
+ }
+
+ goUpdateCommandState("cmd_backgroundColor");
+ }
+ gContentWindow.focus();
+}
+/* eslint-enable complexity */
+
+function GetParentTable(element) {
+ var node = element;
+ while (node) {
+ if (node.nodeName.toLowerCase() == "table") {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+ return node;
+}
+
+function GetParentTableCell(element) {
+ var node = element;
+ while (node) {
+ if (
+ node.nodeName.toLowerCase() == "td" ||
+ node.nodeName.toLowerCase() == "th"
+ ) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+ return node;
+}
+
+function EditorDblClick(event) {
+ // Only bring up properties if clicked on an element or selected link
+ let element = event.target;
+ // We use "href" instead of "a" to not be fooled by named anchor
+ if (!element) {
+ try {
+ element = GetCurrentEditor().getSelectedElement("href");
+ } catch (e) {}
+ }
+
+ // Don't fire for body/p and other block elements.
+ // It's common that people try to double-click
+ // to select a word, but the click hits an empty area.
+ if (
+ element &&
+ ![
+ "body",
+ "p",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "blockquote",
+ "div",
+ "pre",
+ ].includes(element.nodeName.toLowerCase())
+ ) {
+ goDoCommand("cmd_objectProperties");
+ event.preventDefault();
+ }
+}
+
+/* TODO: We need an oncreate hook to do enabling/disabling for the
+ Format menu. There should be code like this for the
+ object-specific "Properties" item
+*/
+// For property dialogs, we want the selected element,
+// but will accept a parent link, list, or table cell if inside one
+function GetObjectForProperties() {
+ var editor = GetCurrentEditor();
+ if (!editor || !IsHTMLEditor()) {
+ return null;
+ }
+
+ var element;
+ try {
+ element = editor.getSelectedElement("");
+ } catch (e) {}
+ if (element) {
+ if (element.namespaceURI == "http://www.w3.org/1998/Math/MathML") {
+ // If the object is a MathML element, we collapse the selection on it and
+ // we return its <math> ancestor. Hence the math dialog will be used.
+ GetCurrentEditor().selection.collapse(element, 0);
+ } else {
+ return element;
+ }
+ }
+
+ // Find nearest parent of selection anchor node
+ // that is a link, list, table cell, or table
+
+ var anchorNode;
+ var node;
+ try {
+ anchorNode = editor.selection.anchorNode;
+ if (anchorNode.firstChild) {
+ // Start at actual selected node
+ var offset = editor.selection.anchorOffset;
+ // Note: If collapsed, offset points to element AFTER caret,
+ // thus node may be null
+ node = anchorNode.childNodes.item(offset);
+ }
+ if (!node) {
+ node = anchorNode;
+ }
+ } catch (e) {}
+
+ while (node) {
+ if (node.nodeName) {
+ var nodeName = node.nodeName.toLowerCase();
+
+ // Done when we hit the body or #text.
+ if (nodeName == "body" || nodeName == "#text") {
+ break;
+ }
+
+ if (
+ (nodeName == "a" && node.href) ||
+ nodeName == "ol" ||
+ nodeName == "ul" ||
+ nodeName == "dl" ||
+ nodeName == "td" ||
+ nodeName == "th" ||
+ nodeName == "table" ||
+ nodeName == "math"
+ ) {
+ return node;
+ }
+ }
+ node = node.parentNode;
+ }
+ return null;
+}
+
+function UpdateWindowTitle() {
+ try {
+ var filename = "";
+ var windowTitle = "";
+ var title = document.title;
+
+ // Append just the 'leaf' filename to the Doc. Title for the window caption
+ var docUrl = GetDocumentUrl();
+ if (docUrl && !IsUrlAboutBlank(docUrl)) {
+ var scheme = GetScheme(docUrl);
+ filename = GetFilename(docUrl);
+ if (filename) {
+ windowTitle = " [" + scheme + ":/.../" + filename + "]";
+ }
+
+ var fileType = IsHTMLEditor() ? "html" : "text";
+ // Save changed title in the recent pages data in prefs
+ SaveRecentFilesPrefs(title, fileType);
+ }
+
+ document.title = (title || filename) + windowTitle;
+ } catch (e) {
+ dump(e);
+ }
+}
+
+function SaveRecentFilesPrefs(aTitle, aFileType) {
+ var curUrl = StripPassword(GetDocumentUrl());
+ var historyCount = Services.prefs.getIntPref("editor.history.url_maximum");
+
+ var titleArray = [];
+ var urlArray = [];
+ var typeArray = [];
+
+ if (historyCount && !IsUrlAboutBlank(curUrl) && GetScheme(curUrl) != "data") {
+ titleArray.push(aTitle);
+ urlArray.push(curUrl);
+ typeArray.push(aFileType);
+ }
+
+ for (let i = 0; i < historyCount && urlArray.length < historyCount; i++) {
+ let url = Services.prefs.getStringPref("editor.history_url_" + i, "");
+
+ // Continue if URL pref is missing because
+ // a URL not found during loading may have been removed
+
+ // Skip over current an "data" URLs
+ if (url && url != curUrl && GetScheme(url) != "data") {
+ let title = Services.prefs.getStringPref("editor.history_title_" + i, "");
+ let fileType = Services.prefs.getStringPref(
+ "editor.history_type_" + i,
+ ""
+ );
+ titleArray.push(title);
+ urlArray.push(url);
+ typeArray.push(fileType);
+ }
+ }
+
+ // Resave the list back to prefs in the new order
+ for (let i = 0; i < urlArray.length; i++) {
+ SetStringPref("editor.history_title_" + i, titleArray[i]);
+ SetStringPref("editor.history_url_" + i, urlArray[i]);
+ SetStringPref("editor.history_type_" + i, typeArray[i]);
+ }
+}
+
+function EditorInitFormatMenu() {
+ try {
+ InitObjectPropertiesMenuitem();
+ InitRemoveStylesMenuitems(
+ "removeStylesMenuitem",
+ "removeLinksMenuitem",
+ "removeNamedAnchorsMenuitem"
+ );
+ } catch (ex) {}
+}
+
+function InitObjectPropertiesMenuitem() {
+ // Set strings and enable for the [Object] Properties item
+ // Note that we directly do the enabling instead of
+ // using goSetCommandEnabled since we already have the command.
+ var cmd = document.getElementById("cmd_objectProperties");
+ if (!cmd) {
+ return null;
+ }
+
+ var element;
+ var menuStr = GetString("AdvancedProperties");
+ var name;
+
+ if (IsEditingRenderedHTML()) {
+ element = GetObjectForProperties();
+ }
+
+ if (element && element.nodeName) {
+ var objStr = "";
+ cmd.removeAttribute("disabled");
+ name = element.nodeName.toLowerCase();
+ switch (name) {
+ case "img":
+ // Check if img is enclosed in link
+ // (use "href" to not be fooled by named anchor)
+ try {
+ if (GetCurrentEditor().getElementOrParentByTagName("href", element)) {
+ objStr = GetString("ImageAndLink");
+ // Return "href" so it is detected as a link.
+ name = "href";
+ }
+ } catch (e) {}
+
+ if (objStr == "") {
+ objStr = GetString("Image");
+ }
+ break;
+ case "hr":
+ objStr = GetString("HLine");
+ break;
+ case "table":
+ objStr = GetString("Table");
+ break;
+ case "th":
+ name = "td";
+ // Falls through
+ case "td":
+ objStr = GetString("TableCell");
+ break;
+ case "ol":
+ case "ul":
+ case "dl":
+ objStr = GetString("List");
+ break;
+ case "li":
+ objStr = GetString("ListItem");
+ break;
+ case "form":
+ objStr = GetString("Form");
+ break;
+ case "input":
+ var type = element.getAttribute("type");
+ if (type && type.toLowerCase() == "image") {
+ objStr = GetString("InputImage");
+ } else {
+ objStr = GetString("InputTag");
+ }
+ break;
+ case "textarea":
+ objStr = GetString("TextArea");
+ break;
+ case "select":
+ objStr = GetString("Select");
+ break;
+ case "button":
+ objStr = GetString("Button");
+ break;
+ case "label":
+ objStr = GetString("Label");
+ break;
+ case "fieldset":
+ objStr = GetString("FieldSet");
+ break;
+ case "a":
+ if (element.name) {
+ objStr = GetString("NamedAnchor");
+ name = "anchor";
+ } else if (element.href) {
+ objStr = GetString("Link");
+ name = "href";
+ }
+ break;
+ }
+ if (objStr) {
+ menuStr = GetString("ObjectProperties").replace(/%obj%/, objStr);
+ }
+ } else {
+ // We show generic "Properties" string, but disable the command.
+ cmd.setAttribute("disabled", "true");
+ }
+ cmd.setAttribute("label", menuStr);
+ cmd.setAttribute("accesskey", GetString("ObjectPropertiesAccessKey"));
+ return name;
+}
+
+function InitParagraphMenu() {
+ var mixedObj = { value: null };
+ var state;
+ try {
+ state = GetCurrentEditor().getParagraphState(mixedObj);
+ } catch (e) {}
+ var IDSuffix;
+
+ // PROBLEM: When we get blockquote, it masks other styles contained by it
+ // We need a separate method to get blockquote state
+
+ // We use "x" as uninitialized paragraph state
+ if (!state || state == "x") {
+ // No paragraph container.
+ IDSuffix = "bodyText";
+ } else {
+ IDSuffix = state;
+ }
+
+ // Set "radio" check on one item, but...
+ var menuItem = document.getElementById("menu_" + IDSuffix);
+ menuItem.setAttribute("checked", "true");
+
+ // ..."bodyText" is returned if mixed selection, so remove checkmark
+ if (mixedObj.value) {
+ menuItem.setAttribute("checked", "false");
+ }
+}
+
+function GetListStateString() {
+ try {
+ var editor = GetCurrentEditor();
+
+ var mixedObj = { value: null };
+ var hasOL = { value: false };
+ var hasUL = { value: false };
+ var hasDL = { value: false };
+ editor.getListState(mixedObj, hasOL, hasUL, hasDL);
+
+ if (mixedObj.value) {
+ return "mixed";
+ }
+ if (hasOL.value) {
+ return "ol";
+ }
+ if (hasUL.value) {
+ return "ul";
+ }
+
+ if (hasDL.value) {
+ var hasLI = { value: false };
+ var hasDT = { value: false };
+ var hasDD = { value: false };
+ editor.getListItemState(mixedObj, hasLI, hasDT, hasDD);
+ if (mixedObj.value) {
+ return "mixed";
+ }
+ if (hasLI.value) {
+ return "li";
+ }
+ if (hasDT.value) {
+ return "dt";
+ }
+ if (hasDD.value) {
+ return "dd";
+ }
+ }
+ } catch (e) {}
+
+ // return "noList" if we aren't in a list at all
+ return "noList";
+}
+
+function InitListMenu() {
+ if (!IsHTMLEditor()) {
+ return;
+ }
+
+ var IDSuffix = GetListStateString();
+
+ // Set enable state for the "None" menuitem
+ goSetCommandEnabled("cmd_removeList", IDSuffix != "noList");
+
+ // Set "radio" check on one item, but...
+ // we won't find a match if it's "mixed"
+ var menuItem = document.getElementById("menu_" + IDSuffix);
+ if (menuItem) {
+ menuItem.setAttribute("checked", "true");
+ }
+}
+
+function GetAlignmentString() {
+ var mixedObj = { value: null };
+ var alignObj = { value: null };
+ try {
+ GetCurrentEditor().getAlignment(mixedObj, alignObj);
+ } catch (e) {}
+
+ if (mixedObj.value) {
+ return "mixed";
+ }
+ if (alignObj.value == Ci.nsIHTMLEditor.eLeft) {
+ return "left";
+ }
+ if (alignObj.value == Ci.nsIHTMLEditor.eCenter) {
+ return "center";
+ }
+ if (alignObj.value == Ci.nsIHTMLEditor.eRight) {
+ return "right";
+ }
+ if (alignObj.value == Ci.nsIHTMLEditor.eJustify) {
+ return "justify";
+ }
+
+ // return "left" if we got here
+ return "left";
+}
+
+function InitAlignMenu() {
+ if (!IsHTMLEditor()) {
+ return;
+ }
+
+ var IDSuffix = GetAlignmentString();
+
+ // we won't find a match if it's "mixed"
+ var menuItem = document.getElementById("menu_" + IDSuffix);
+ if (menuItem) {
+ menuItem.setAttribute("checked", "true");
+ }
+}
+
+function EditorSetDefaultPrefsAndDoctype() {
+ var editor = GetCurrentEditor();
+
+ var domdoc;
+ try {
+ domdoc = editor.document;
+ } catch (e) {
+ dump(e + "\n");
+ }
+ if (!domdoc) {
+ dump("EditorSetDefaultPrefsAndDoctype: EDITOR DOCUMENT NOT FOUND\n");
+ return;
+ }
+
+ // Insert a doctype element
+ // if it is missing from existing doc
+ if (!domdoc.doctype) {
+ var newdoctype = domdoc.implementation.createDocumentType(
+ "HTML",
+ "-//W3C//DTD HTML 4.01 Transitional//EN",
+ ""
+ );
+ if (newdoctype) {
+ domdoc.insertBefore(newdoctype, domdoc.firstChild);
+ }
+ }
+
+ // search for head; we'll need this for meta tag additions
+ let headelement = domdoc.querySelector("head");
+ if (!headelement) {
+ headelement = domdoc.createElement("head");
+ domdoc.insertAfter(headelement, domdoc.firstChild);
+ }
+
+ /* only set default prefs for new documents */
+ if (!IsUrlAboutBlank(GetDocumentUrl())) {
+ return;
+ }
+
+ // search for author meta tag.
+ // if one is found, don't do anything.
+ // if not, create one and make it a child of the head tag
+ // and set its content attribute to the value of the editor.author preference.
+
+ if (domdoc.querySelector("meta")) {
+ // we should do charset first since we need to have charset before
+ // hitting other 8-bit char in other meta tags
+ // grab charset pref and make it the default charset
+ var element;
+ var prefCharsetString = Services.prefs.getCharPref(
+ "intl.charset.fallback.override"
+ );
+ if (prefCharsetString) {
+ editor.documentCharacterSet = prefCharsetString;
+ }
+
+ // let's start by assuming we have an author in case we don't have the pref
+
+ var prefAuthorString = null;
+ let authorFound = domdoc.querySelector('meta[name="author"]');
+ try {
+ prefAuthorString = Services.prefs.getStringPref("editor.author");
+ } catch (ex) {}
+ if (
+ prefAuthorString &&
+ prefAuthorString != 0 &&
+ !authorFound &&
+ headelement
+ ) {
+ // create meta tag with 2 attributes
+ element = domdoc.createElement("meta");
+ if (element) {
+ element.setAttribute("name", "author");
+ element.setAttribute("content", prefAuthorString);
+ headelement.appendChild(element);
+ }
+ }
+ }
+
+ // add title tag if not present
+ if (headelement && !editor.document.querySelector("title")) {
+ var titleElement = domdoc.createElement("title");
+ if (titleElement) {
+ headelement.appendChild(titleElement);
+ }
+ }
+
+ // find body node
+ var bodyelement = GetBodyElement();
+ if (bodyelement) {
+ if (Services.prefs.getBoolPref("editor.use_custom_colors")) {
+ let text_color = Services.prefs.getCharPref("editor.text_color");
+ let background_color = Services.prefs.getCharPref(
+ "editor.background_color"
+ );
+
+ // add the color attributes to the body tag.
+ // and use them for the default text and background colors if not empty
+ editor.setAttributeOrEquivalent(bodyelement, "text", text_color, true);
+ gDefaultTextColor = text_color;
+ editor.setAttributeOrEquivalent(
+ bodyelement,
+ "bgcolor",
+ background_color,
+ true
+ );
+ gDefaultBackgroundColor = background_color;
+ bodyelement.setAttribute(
+ "link",
+ Services.prefs.getCharPref("editor.link_color")
+ );
+ bodyelement.setAttribute(
+ "alink",
+ Services.prefs.getCharPref("editor.active_link_color")
+ );
+ bodyelement.setAttribute(
+ "vlink",
+ Services.prefs.getCharPref("editor.followed_link_color")
+ );
+ }
+ // Default image is independent of Custom colors???
+ try {
+ let background_image = Services.prefs.getCharPref(
+ "editor.default_background_image"
+ );
+ if (background_image) {
+ editor.setAttributeOrEquivalent(
+ bodyelement,
+ "background",
+ background_image,
+ true
+ );
+ }
+ } catch (e) {
+ dump("BACKGROUND EXCEPTION: " + e + "\n");
+ }
+ }
+ // auto-save???
+}
+
+function GetBodyElement() {
+ try {
+ return GetCurrentEditor().rootElement;
+ } catch (ex) {
+ dump("no body tag found?!\n");
+ // better have one, how can we blow things up here?
+ }
+ return null;
+}
+
+// --------------------------------------------------------------------
+function initFontStyleMenu(menuPopup) {
+ for (var i = 0; i < menuPopup.children.length; i++) {
+ var menuItem = menuPopup.children[i];
+ var theStyle = menuItem.getAttribute("state");
+ if (theStyle) {
+ menuItem.setAttribute("checked", theStyle);
+ }
+ }
+}
+
+// -----------------------------------------------------------------------------------
+function IsSpellCheckerInstalled() {
+ return true; // Always installed.
+}
+
+// -----------------------------------------------------------------------------------
+function IsFindInstalled() {
+ return (
+ "@mozilla.org/embedcomp/rangefind;1" in Cc &&
+ "@mozilla.org/find/find_service;1" in Cc
+ );
+}
+
+// -----------------------------------------------------------------------------------
+function RemoveInapplicableUIElements() {
+ // For items that are in their own menu block, remove associated separator
+ // (we can't use "hidden" since class="hide-in-IM" CSS rule interferes)
+
+ // if no find, remove find ui
+ if (!IsFindInstalled()) {
+ HideItem("menu_find");
+ HideItem("menu_findnext");
+ HideItem("menu_replace");
+ HideItem("menu_find");
+ RemoveItem("sep_find");
+ }
+
+ // if no spell checker, remove spell checker ui
+ if (!IsSpellCheckerInstalled()) {
+ HideItem("spellingButton");
+ HideItem("menu_checkspelling");
+ RemoveItem("sep_checkspelling");
+ }
+
+ // Remove menu items (from overlay shared with HTML editor) in non-HTML.
+ if (!IsHTMLEditor()) {
+ HideItem("insertAnchor");
+ HideItem("insertImage");
+ HideItem("insertHline");
+ HideItem("insertTable");
+ HideItem("insertHTML");
+ HideItem("insertFormMenu");
+ HideItem("fileExportToText");
+ HideItem("viewFormatToolbar");
+ HideItem("viewEditModeToolbar");
+ }
+}
+
+function HideItem(id) {
+ var item = document.getElementById(id);
+ if (item) {
+ item.hidden = true;
+ }
+}
+
+function RemoveItem(id) {
+ var item = document.getElementById(id);
+ if (item) {
+ item.remove();
+ }
+}
+
+// Command Updating Strategy:
+// Don't update on on selection change, only when menu is displayed,
+// with this "oncreate" handler:
+function EditorInitTableMenu() {
+ try {
+ InitJoinCellMenuitem("menu_JoinTableCells");
+ } catch (ex) {}
+
+ // Set enable states for all table commands
+ goUpdateTableMenuItems(document.getElementById("composerTableMenuItems"));
+}
+
+function InitJoinCellMenuitem(id) {
+ // Change text on the "Join..." item depending if we
+ // are joining selected cells or just cell to right
+ // TODO: What to do about normal selection that crosses
+ // table border? Try to figure out all cells
+ // included in the selection?
+ var menuText;
+ var menuItem = document.getElementById(id);
+ if (!menuItem) {
+ return;
+ }
+
+ // Use "Join selected cells if there's more than 1 cell selected
+ var numSelected;
+ var foundElement;
+
+ try {
+ var tagNameObj = {};
+ var countObj = { value: 0 };
+ foundElement = GetCurrentTableEditor().getSelectedOrParentTableElement(
+ tagNameObj,
+ countObj
+ );
+ numSelected = countObj.value;
+ } catch (e) {}
+ if (foundElement && numSelected > 1) {
+ menuText = GetString("JoinSelectedCells");
+ } else {
+ menuText = GetString("JoinCellToRight");
+ }
+
+ menuItem.setAttribute("label", menuText);
+ menuItem.setAttribute("accesskey", GetString("JoinCellAccesskey"));
+}
+
+function InitRemoveStylesMenuitems(
+ removeStylesId,
+ removeLinksId,
+ removeNamedAnchorsId
+) {
+ var editor = GetCurrentEditor();
+ if (!editor) {
+ return;
+ }
+
+ // Change wording of menuitems depending on selection
+ var stylesItem = document.getElementById(removeStylesId);
+ var linkItem = document.getElementById(removeLinksId);
+
+ var isCollapsed = editor.selection.isCollapsed;
+ if (stylesItem) {
+ stylesItem.setAttribute(
+ "label",
+ isCollapsed ? GetString("StopTextStyles") : GetString("RemoveTextStyles")
+ );
+ stylesItem.setAttribute(
+ "accesskey",
+ GetString("RemoveTextStylesAccesskey")
+ );
+ }
+ if (linkItem) {
+ linkItem.setAttribute(
+ "label",
+ isCollapsed ? GetString("StopLinks") : GetString("RemoveLinks")
+ );
+ linkItem.setAttribute("accesskey", GetString("RemoveLinksAccesskey"));
+ // Note: disabling text style is a pain since there are so many - forget it!
+
+ // Disable if not in a link, but always allow "Remove"
+ // if selection isn't collapsed since we only look at anchor node
+ try {
+ SetElementEnabled(
+ linkItem,
+ !isCollapsed || editor.getElementOrParentByTagName("href", null)
+ );
+ } catch (e) {}
+ }
+ // Disable if selection is collapsed
+ SetElementEnabledById(removeNamedAnchorsId, !isCollapsed);
+}
+
+function goUpdateTableMenuItems(commandset) {
+ var editor = GetCurrentTableEditor();
+ if (!editor) {
+ dump("goUpdateTableMenuItems: too early, not initialized\n");
+ return;
+ }
+
+ var enabled = false;
+ var enabledIfTable = false;
+
+ var flags = editor.flags;
+ if (!(flags & Ci.nsIEditor.eEditorReadonlyMask) && IsEditingRenderedHTML()) {
+ var tagNameObj = { value: "" };
+ var element;
+ try {
+ element = editor.getSelectedOrParentTableElement(tagNameObj, {
+ value: 0,
+ });
+ } catch (e) {}
+
+ if (element) {
+ // Value when we need to have a selected table or inside a table
+ enabledIfTable = true;
+
+ // All others require being inside a cell or selected cell
+ enabled = tagNameObj.value == "td";
+ }
+ }
+
+ // Loop through command nodes
+ for (var i = 0; i < commandset.children.length; i++) {
+ var commandID = commandset.children[i].getAttribute("id");
+ if (commandID) {
+ if (
+ commandID == "cmd_InsertTable" ||
+ commandID == "cmd_JoinTableCells" ||
+ commandID == "cmd_SplitTableCell" ||
+ commandID == "cmd_ConvertToTable"
+ ) {
+ // Call the update method in the command class
+ goUpdateCommand(commandID);
+ } else if (
+ commandID == "cmd_DeleteTable" ||
+ commandID == "cmd_editTable" ||
+ commandID == "cmd_TableOrCellColor" ||
+ commandID == "cmd_SelectTable"
+ ) {
+ // Directly set with the values calculated here
+ goSetCommandEnabled(commandID, enabledIfTable);
+ } else {
+ goSetCommandEnabled(commandID, enabled);
+ }
+ }
+ }
+}
+
+// -----------------------------------------------------------------------------------
+// Helpers for inserting and editing tables:
+
+function IsInTable() {
+ var editor = GetCurrentEditor();
+ try {
+ var flags = editor.flags;
+ return (
+ IsHTMLEditor() &&
+ !(flags & Ci.nsIEditor.eEditorReadonlyMask) &&
+ IsEditingRenderedHTML() &&
+ null != editor.getElementOrParentByTagName("table", null)
+ );
+ } catch (e) {}
+ return false;
+}
+
+function IsInTableCell() {
+ try {
+ var editor = GetCurrentEditor();
+ var flags = editor.flags;
+ return (
+ IsHTMLEditor() &&
+ !(flags & Ci.nsIEditor.eEditorReadonlyMask) &&
+ IsEditingRenderedHTML() &&
+ null != editor.getElementOrParentByTagName("td", null)
+ );
+ } catch (e) {}
+ return false;
+}
+
+function IsSelectionInOneCell() {
+ try {
+ var editor = GetCurrentEditor();
+ var selection = editor.selection;
+
+ if (selection.rangeCount == 1) {
+ // We have a "normal" single-range selection
+ if (
+ !selection.isCollapsed &&
+ selection.anchorNode != selection.focusNode
+ ) {
+ // Check if both nodes are within the same cell
+ var anchorCell = editor.getElementOrParentByTagName(
+ "td",
+ selection.anchorNode
+ );
+ var focusCell = editor.getElementOrParentByTagName(
+ "td",
+ selection.focusNode
+ );
+ return (
+ focusCell != null && anchorCell != null && focusCell == anchorCell
+ );
+ }
+ // Collapsed selection or anchor == focus (thus must be in 1 cell)
+ return true;
+ }
+ } catch (e) {}
+ return false;
+}
+
+// Call this with insertAllowed = true to allow inserting if not in existing table,
+// else use false to do nothing if not in a table
+function EditorInsertOrEditTable(insertAllowed) {
+ if (IsInTable()) {
+ // Edit properties of existing table
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdTableProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ "",
+ "TablePanel"
+ );
+ gContentWindow.focus();
+ } else if (insertAllowed) {
+ try {
+ if (GetCurrentEditor().selection.isCollapsed) {
+ // If we have a caret, insert a blank table...
+ EditorInsertTable();
+ } else {
+ // Else convert the selection into a table.
+ goDoCommand("cmd_ConvertToTable");
+ }
+ } catch (e) {}
+ }
+}
+
+function EditorInsertTable() {
+ // Insert a new table
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdInsertTable.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ ""
+ );
+ gContentWindow.focus();
+}
+
+function EditorTableCellProperties() {
+ if (!IsHTMLEditor()) {
+ return;
+ }
+
+ try {
+ var cell = GetCurrentEditor().getElementOrParentByTagName("td", null);
+ if (cell) {
+ // Start Table Properties dialog on the "Cell" panel
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdTableProps.xhtml",
+ "_blank",
+ "chrome,close,titlebar,modal",
+ "",
+ "CellPanel"
+ );
+ gContentWindow.focus();
+ }
+ } catch (e) {}
+}
+
+function GetNumberOfContiguousSelectedRows() {
+ if (!IsHTMLEditor()) {
+ return 0;
+ }
+
+ var rows = 0;
+ var editor = GetCurrentTableEditor();
+ var rowObj = { value: 0 };
+ var colObj = { value: 0 };
+ var cell = editor.getFirstSelectedCellInTable(rowObj, colObj);
+ if (!cell) {
+ return 0;
+ }
+
+ // We have at least one row
+ rows++;
+
+ var lastIndex = rowObj.value;
+ for (let cell of editor.getSelectedCells()) {
+ editor.getCellIndexes(cell, rowObj, colObj);
+ var index = rowObj.value;
+ if (index == lastIndex + 1) {
+ lastIndex = index;
+ rows++;
+ }
+ }
+
+ return rows;
+}
+
+function GetNumberOfContiguousSelectedColumns() {
+ if (!IsHTMLEditor()) {
+ return 0;
+ }
+
+ var columns = 0;
+
+ var editor = GetCurrentTableEditor();
+ var colObj = { value: 0 };
+ var rowObj = { value: 0 };
+ var cell = editor.getFirstSelectedCellInTable(rowObj, colObj);
+ if (!cell) {
+ return 0;
+ }
+
+ // We have at least one column
+ columns++;
+
+ var lastIndex = colObj.value;
+ for (let cell of editor.getSelectedCells()) {
+ editor.getCellIndexes(cell, rowObj, colObj);
+ var index = colObj.value;
+ if (index == lastIndex + 1) {
+ lastIndex = index;
+ columns++;
+ }
+ }
+
+ return columns;
+}
+
+function EditorOnFocus() {
+ // Current window already has the InsertCharWindow
+ if ("InsertCharWindow" in window && window.InsertCharWindow) {
+ return;
+ }
+
+ // Find window with an InsertCharsWindow and switch association to this one
+ var windowWithDialog = FindEditorWithInsertCharDialog();
+ if (windowWithDialog) {
+ // Switch the dialog to current window
+ // this sets focus to dialog, so bring focus back to editor window
+ if (SwitchInsertCharToThisWindow(windowWithDialog)) {
+ top.document.commandDispatcher.focusedWindow.focus();
+ }
+ }
+}
+
+function SwitchInsertCharToThisWindow(windowWithDialog) {
+ if (
+ windowWithDialog &&
+ "InsertCharWindow" in windowWithDialog &&
+ windowWithDialog.InsertCharWindow
+ ) {
+ // Move dialog association to the current window
+ window.InsertCharWindow = windowWithDialog.InsertCharWindow;
+ windowWithDialog.InsertCharWindow = null;
+
+ // Switch the dialog's opener to current window's
+ window.InsertCharWindow.opener = window;
+
+ // Bring dialog to the foreground
+ window.InsertCharWindow.focus();
+ return true;
+ }
+ return false;
+}
+
+function FindEditorWithInsertCharDialog() {
+ try {
+ // Find window with an InsertCharsWindow and switch association to this one
+
+ for (let tempWindow of Services.wm.getEnumerator(null)) {
+ if (
+ !tempWindow.closed &&
+ tempWindow != window &&
+ "InsertCharWindow" in tempWindow &&
+ tempWindow.InsertCharWindow
+ ) {
+ return tempWindow;
+ }
+ }
+ } catch (e) {}
+ return null;
+}
+
+function EditorFindOrCreateInsertCharWindow() {
+ if ("InsertCharWindow" in window && window.InsertCharWindow) {
+ window.InsertCharWindow.focus();
+ } else {
+ // Since we switch the dialog during EditorOnFocus(),
+ // this should really never be found, but it's good to be sure
+ var windowWithDialog = FindEditorWithInsertCharDialog();
+ if (windowWithDialog) {
+ SwitchInsertCharToThisWindow(windowWithDialog);
+ } else {
+ // The dialog will set window.InsertCharWindow to itself
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdInsertChars.xhtml",
+ "_blank",
+ "chrome,close,titlebar",
+ ""
+ );
+ }
+ }
+}
+
+// Find another HTML editor window to associate with the InsertChar dialog
+// or close it if none found (May be a mail composer)
+function SwitchInsertCharToAnotherEditorOrClose() {
+ if ("InsertCharWindow" in window && window.InsertCharWindow) {
+ var enumerator;
+ try {
+ enumerator = Services.wm.getEnumerator(null);
+ } catch (e) {}
+ if (!enumerator) {
+ return;
+ }
+
+ // TODO: Fix this to search for command controllers and look for "cmd_InsertChars"
+ // For now, detect just Web Composer and HTML Mail Composer
+ for (let tempWindow of enumerator) {
+ if (
+ !tempWindow.closed &&
+ tempWindow != window &&
+ tempWindow != window.InsertCharWindow &&
+ "GetCurrentEditor" in tempWindow &&
+ tempWindow.GetCurrentEditor()
+ ) {
+ tempWindow.InsertCharWindow = window.InsertCharWindow;
+ window.InsertCharWindow = null;
+ tempWindow.InsertCharWindow.opener = tempWindow;
+ return;
+ }
+ }
+ // Didn't find another editor - close the dialog
+ window.InsertCharWindow.close();
+ }
+}
+
+function UpdateTOC() {
+ window.openDialog(
+ "chrome://messenger/content/messengercompose/EdInsertTOC.xhtml",
+ "_blank",
+ "chrome,close,modal,titlebar"
+ );
+ window.content.focus();
+}
+
+function InitTOCMenu() {
+ var elt = GetCurrentEditor().document.getElementById("mozToc");
+ var createMenuitem = document.getElementById("insertTOCMenuitem");
+ var updateMenuitem = document.getElementById("updateTOCMenuitem");
+ var removeMenuitem = document.getElementById("removeTOCMenuitem");
+ if (removeMenuitem && createMenuitem && updateMenuitem) {
+ if (elt) {
+ createMenuitem.setAttribute("disabled", "true");
+ updateMenuitem.removeAttribute("disabled");
+ removeMenuitem.removeAttribute("disabled");
+ } else {
+ createMenuitem.removeAttribute("disabled");
+ removeMenuitem.setAttribute("disabled", "true");
+ updateMenuitem.setAttribute("disabled", "true");
+ }
+ }
+}
+
+function RemoveTOC() {
+ var theDocument = GetCurrentEditor().document;
+ var elt = theDocument.getElementById("mozToc");
+ if (elt) {
+ elt.remove();
+ }
+
+ let anchorNodes = theDocument.querySelectorAll('a[name^="mozTocId"]');
+ for (let node of anchorNodes) {
+ if (node.parentNode) {
+ node.remove();
+ }
+ }
+}
diff --git a/comm/mail/components/compose/content/editorUtilities.js b/comm/mail/components/compose/content/editorUtilities.js
new file mode 100644
index 0000000000..3af6810c9c
--- /dev/null
+++ b/comm/mail/components/compose/content/editorUtilities.js
@@ -0,0 +1,1015 @@
+/* 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 editor.js */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+// Each editor window must include this file
+// Variables shared by all dialogs:
+
+// Object to attach commonly-used widgets (all dialogs should use this)
+var gDialog = {};
+
+var kOutputEncodeBasicEntities =
+ Ci.nsIDocumentEncoder.OutputEncodeBasicEntities;
+var kOutputEncodeHTMLEntities = Ci.nsIDocumentEncoder.OutputEncodeHTMLEntities;
+var kOutputEncodeLatin1Entities =
+ Ci.nsIDocumentEncoder.OutputEncodeLatin1Entities;
+var kOutputEncodeW3CEntities = Ci.nsIDocumentEncoder.OutputEncodeW3CEntities;
+var kOutputFormatted = Ci.nsIDocumentEncoder.OutputFormatted;
+var kOutputLFLineBreak = Ci.nsIDocumentEncoder.OutputLFLineBreak;
+var kOutputSelectionOnly = Ci.nsIDocumentEncoder.OutputSelectionOnly;
+var kOutputWrap = Ci.nsIDocumentEncoder.OutputWrap;
+
+var gStringBundle;
+var gFilePickerDirectory;
+
+/** *********** Message dialogs */
+
+// Optional: Caller may supply text to substitute for "Ok" and/or "Cancel"
+function ConfirmWithTitle(title, message, okButtonText, cancelButtonText) {
+ let okFlag = okButtonText
+ ? Services.prompt.BUTTON_TITLE_IS_STRING
+ : Services.prompt.BUTTON_TITLE_OK;
+ let cancelFlag = cancelButtonText
+ ? Services.prompt.BUTTON_TITLE_IS_STRING
+ : Services.prompt.BUTTON_TITLE_CANCEL;
+
+ return (
+ Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ okFlag * Services.prompt.BUTTON_POS_0 +
+ cancelFlag * Services.prompt.BUTTON_POS_1,
+ okButtonText,
+ cancelButtonText,
+ null,
+ null,
+ { value: 0 }
+ ) == 0
+ );
+}
+
+/** *********** String Utilities */
+
+function GetString(name) {
+ if (!gStringBundle) {
+ try {
+ gStringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/editor.properties"
+ );
+ } catch (ex) {}
+ }
+ if (gStringBundle) {
+ try {
+ return gStringBundle.GetStringFromName(name);
+ } catch (e) {}
+ }
+ return null;
+}
+
+function GetFormattedString(aName, aVal) {
+ if (!gStringBundle) {
+ try {
+ gStringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/editor.properties"
+ );
+ } catch (ex) {}
+ }
+ if (gStringBundle) {
+ try {
+ return gStringBundle.formatStringFromName(aName, [aVal]);
+ } catch (e) {}
+ }
+ return null;
+}
+
+function TrimStringLeft(string) {
+ if (!string) {
+ return "";
+ }
+ return string.trimLeft();
+}
+
+function TrimStringRight(string) {
+ if (!string) {
+ return "";
+ }
+ return string.trimRight();
+}
+
+// Remove whitespace from both ends of a string
+function TrimString(string) {
+ if (!string) {
+ return "";
+ }
+ return string.trim();
+}
+
+function TruncateStringAtWordEnd(string, maxLength, addEllipses) {
+ // Return empty if string is null, undefined, or the empty string
+ if (!string) {
+ return "";
+ }
+
+ // We assume they probably don't want whitespace at the beginning
+ string = string.trimLeft();
+ if (string.length <= maxLength) {
+ return string;
+ }
+
+ // We need to truncate the string to maxLength or fewer chars
+ if (addEllipses) {
+ maxLength -= 3;
+ }
+ string = string.replace(RegExp("(.{0," + maxLength + "})\\s.*"), "$1");
+
+ if (string.length > maxLength) {
+ string = string.slice(0, maxLength);
+ }
+
+ if (addEllipses) {
+ string += "...";
+ }
+ return string;
+}
+
+// Replace all whitespace characters with supplied character
+// E.g.: Use charReplace = " ", to "unwrap" the string by removing line-end chars
+// Use charReplace = "_" when you don't want spaces (like in a URL)
+function ReplaceWhitespace(string, charReplace) {
+ return string.trim().replace(/\s+/g, charReplace);
+}
+
+// Replace whitespace with "_" and allow only HTML CDATA
+// characters: "a"-"z","A"-"Z","0"-"9", "_", ":", "-", ".",
+// and characters above ASCII 127
+function ConvertToCDATAString(string) {
+ return string
+ .replace(/\s+/g, "_")
+ .replace(/[^a-zA-Z0-9_\.\-\:\u0080-\uFFFF]+/g, "");
+}
+
+function GetSelectionAsText() {
+ try {
+ return GetCurrentEditor().outputToString(
+ "text/plain",
+ kOutputSelectionOnly
+ );
+ } catch (e) {}
+
+ return "";
+}
+
+/** *********** Get Current Editor and associated interfaces or info */
+
+function GetCurrentEditor() {
+ // Get the active editor from the <editor> tag
+ // XXX This will probably change if we support > 1 editor in main Composer window
+ // (e.g. a plaintext editor for HTMLSource)
+
+ // For dialogs: Search up parent chain to find top window with editor
+ var editor;
+ try {
+ var editorElement = GetCurrentEditorElement();
+ editor = editorElement.getEditor(editorElement.contentWindow);
+
+ // Do QIs now so editor users won't have to figure out which interface to use
+ // Using "instanceof" does the QI for us.
+ editor instanceof Ci.nsIHTMLEditor;
+ } catch (e) {
+ dump(e) + "\n";
+ }
+
+ return editor;
+}
+
+function GetCurrentTableEditor() {
+ var editor = GetCurrentEditor();
+ return editor && editor instanceof Ci.nsITableEditor ? editor : null;
+}
+
+function GetCurrentEditorElement() {
+ var tmpWindow = window;
+
+ do {
+ // Get the <editor> element(s)
+ let editorItem = tmpWindow.document.querySelector("editor");
+
+ // This will change if we support > 1 editor element
+ if (editorItem) {
+ return editorItem;
+ }
+
+ tmpWindow = tmpWindow.opener;
+ } while (tmpWindow);
+
+ return null;
+}
+
+function GetCurrentCommandManager() {
+ try {
+ return GetCurrentEditorElement().commandManager;
+ } catch (e) {
+ dump(e) + "\n";
+ }
+
+ return null;
+}
+
+function GetCurrentEditorType() {
+ try {
+ return GetCurrentEditorElement().editortype;
+ } catch (e) {
+ dump(e) + "\n";
+ }
+
+ return "";
+}
+
+/**
+ * Gets the editor's spell checker. Could return null if there are no
+ * dictionaries installed.
+ *
+ * @returns {nsIInlineSpellChecker?}
+ */
+function GetCurrentEditorSpellChecker() {
+ try {
+ return GetCurrentEditor().getInlineSpellChecker(true);
+ } catch (ex) {}
+ return null;
+}
+
+function IsHTMLEditor() {
+ // We don't have an editorElement, just return false
+ if (!GetCurrentEditorElement()) {
+ return false;
+ }
+
+ var editortype = GetCurrentEditorType();
+ switch (editortype) {
+ case "html":
+ case "htmlmail":
+ return true;
+
+ case "text":
+ case "textmail":
+ return false;
+
+ default:
+ dump("INVALID EDITOR TYPE: " + editortype + "\n");
+ break;
+ }
+ return false;
+}
+
+function PageIsEmptyAndUntouched() {
+ return IsDocumentEmpty() && !IsDocumentModified() && !IsHTMLSourceChanged();
+}
+
+function IsInHTMLSourceMode() {
+ return gEditorDisplayMode == kDisplayModeSource;
+}
+
+// are we editing HTML (i.e. neither in HTML source mode, nor editing a text file)
+function IsEditingRenderedHTML() {
+ return IsHTMLEditor() && !IsInHTMLSourceMode();
+}
+
+function IsDocumentEditable() {
+ try {
+ return GetCurrentEditor().isDocumentEditable;
+ } catch (e) {}
+ return false;
+}
+
+function IsDocumentEmpty() {
+ try {
+ return GetCurrentEditor().documentIsEmpty;
+ } catch (e) {}
+ return false;
+}
+
+function IsDocumentModified() {
+ try {
+ return GetCurrentEditor().documentModified;
+ } catch (e) {}
+ return false;
+}
+
+function IsHTMLSourceChanged() {
+ // gSourceTextEditor will not be defined if we're just a text editor.
+ return gSourceTextEditor ? gSourceTextEditor.documentModified : false;
+}
+
+function newCommandParams() {
+ try {
+ return Cu.createCommandParams();
+ } catch (e) {
+ dump("error thrown in newCommandParams: " + e + "\n");
+ }
+ return null;
+}
+
+/** *********** General editing command utilities */
+
+function GetDocumentTitle() {
+ try {
+ return GetCurrentEditorElement().contentDocument.title;
+ } catch (e) {}
+
+ return "";
+}
+
+function SetDocumentTitle(title) {
+ try {
+ GetCurrentEditorElement().contentDocument.title = title;
+
+ // Update window title (doesn't work if called from a dialog)
+ if ("UpdateWindowTitle" in window) {
+ window.UpdateWindowTitle();
+ }
+ } catch (e) {}
+}
+
+function EditorGetTextProperty(
+ property,
+ attribute,
+ value,
+ firstHas,
+ anyHas,
+ allHas
+) {
+ try {
+ return GetCurrentEditor().getInlinePropertyWithAttrValue(
+ property,
+ attribute,
+ value,
+ firstHas,
+ anyHas,
+ allHas
+ );
+ } catch (e) {}
+}
+
+function EditorSetTextProperty(property, attribute, value) {
+ try {
+ GetCurrentEditor().setInlineProperty(property, attribute, value);
+ if ("gContentWindow" in window) {
+ window.gContentWindow.focus();
+ }
+ } catch (e) {}
+}
+
+function EditorRemoveTextProperty(property, attribute) {
+ try {
+ GetCurrentEditor().removeInlineProperty(property, attribute);
+ if ("gContentWindow" in window) {
+ window.gContentWindow.focus();
+ }
+ } catch (e) {}
+}
+
+/** *********** Element enbabling/disabling */
+
+// this function takes an elementID and a flag
+// if the element can be found by ID, then it is either enabled (by removing "disabled" attr)
+// or disabled (setAttribute) as specified in the "doEnable" parameter
+function SetElementEnabledById(elementID, doEnable) {
+ SetElementEnabled(document.getElementById(elementID), doEnable);
+}
+
+function SetElementEnabled(element, doEnable) {
+ if (element) {
+ if (doEnable) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+ } else {
+ dump("Element not found in SetElementEnabled\n");
+ }
+}
+
+/** *********** Services / Prefs */
+
+function GetFileProtocolHandler() {
+ let handler = Services.io.getProtocolHandler("file");
+ return handler.QueryInterface(Ci.nsIFileProtocolHandler);
+}
+
+function SetStringPref(aPrefName, aPrefValue) {
+ try {
+ Services.prefs.setStringPref(aPrefName, aPrefValue);
+ } catch (e) {}
+}
+
+// Set initial directory for a filepicker from URLs saved in prefs
+function SetFilePickerDirectory(filePicker, fileType) {
+ if (filePicker) {
+ try {
+ // Save current directory so we can reset it in SaveFilePickerDirectory
+ gFilePickerDirectory = filePicker.displayDirectory;
+
+ let location = Services.prefs.getComplexValue(
+ "editor.lastFileLocation." + fileType,
+ Ci.nsIFile
+ );
+ if (location) {
+ filePicker.displayDirectory = location;
+ }
+ } catch (e) {}
+ }
+}
+
+// Save the directory of the selected file to prefs
+function SaveFilePickerDirectory(filePicker, fileType) {
+ if (filePicker && filePicker.file) {
+ try {
+ var fileDir;
+ if (filePicker.file.parent) {
+ fileDir = filePicker.file.parent.QueryInterface(Ci.nsIFile);
+ }
+
+ Services.prefs.setComplexValue(
+ "editor.lastFileLocation." + fileType,
+ Ci.nsIFile,
+ fileDir
+ );
+
+ Services.prefs.savePrefFile(null);
+ } catch (e) {}
+ }
+
+ // Restore the directory used before SetFilePickerDirectory was called;
+ // This reduces interference with Browser and other module directory defaults
+ if (gFilePickerDirectory) {
+ filePicker.displayDirectory = gFilePickerDirectory;
+ }
+
+ gFilePickerDirectory = null;
+}
+
+function GetDefaultBrowserColors() {
+ var colors = {
+ TextColor: 0,
+ BackgroundColor: 0,
+ LinkColor: 0,
+ ActiveLinkColor: 0,
+ VisitedLinkColor: 0,
+ };
+ var useSysColors = Services.prefs.getBoolPref(
+ "browser.display.use_system_colors",
+ false
+ );
+
+ if (!useSysColors) {
+ colors.TextColor = Services.prefs.getCharPref(
+ "browser.display.foreground_color",
+ 0
+ );
+ colors.BackgroundColor = Services.prefs.getCharPref(
+ "browser.display.background_color",
+ 0
+ );
+ }
+ // Use OS colors for text and background if explicitly asked or pref is not set
+ if (!colors.TextColor) {
+ colors.TextColor = "windowtext";
+ }
+
+ if (!colors.BackgroundColor) {
+ colors.BackgroundColor = "window";
+ }
+
+ colors.LinkColor = Services.prefs.getCharPref("browser.anchor_color");
+ colors.ActiveLinkColor = Services.prefs.getCharPref("browser.active_color");
+ colors.VisitedLinkColor = Services.prefs.getCharPref("browser.visited_color");
+
+ return colors;
+}
+
+/** *********** URL handling */
+
+function TextIsURI(selectedText) {
+ return (
+ selectedText &&
+ /^http:\/\/|^https:\/\/|^file:\/\/|^ftp:\/\/|^about:|^mailto:|^news:|^snews:|^telnet:|^ldap:|^ldaps:|^gopher:|^finger:|^javascript:/i.test(
+ selectedText
+ )
+ );
+}
+
+function IsUrlAboutBlank(urlString) {
+ return urlString.startsWith("about:blank");
+}
+
+function MakeRelativeUrl(url) {
+ let inputUrl = url.trim();
+ if (!inputUrl) {
+ return inputUrl;
+ }
+
+ // Get the filespec relative to current document's location
+ // NOTE: Can't do this if file isn't saved yet!
+ var docUrl = GetDocumentBaseUrl();
+ var docScheme = GetScheme(docUrl);
+
+ // Can't relativize if no doc scheme (page hasn't been saved)
+ if (!docScheme) {
+ return inputUrl;
+ }
+
+ var urlScheme = GetScheme(inputUrl);
+
+ // Do nothing if not the same scheme or url is already relativized
+ if (docScheme != urlScheme) {
+ return inputUrl;
+ }
+
+ // Host must be the same
+ var docHost = GetHost(docUrl);
+ var urlHost = GetHost(inputUrl);
+ if (docHost != urlHost) {
+ return inputUrl;
+ }
+
+ // Get just the file path part of the urls
+ // XXX Should we use GetCurrentEditor().documentCharacterSet for 2nd param ?
+ let docPath = Services.io.newURI(
+ docUrl,
+ GetCurrentEditor().documentCharacterSet
+ ).pathQueryRef;
+ let urlPath = Services.io.newURI(
+ inputUrl,
+ GetCurrentEditor().documentCharacterSet
+ ).pathQueryRef;
+
+ // We only return "urlPath", so we can convert the entire docPath for
+ // case-insensitive comparisons.
+ var doCaseInsensitive = docScheme == "file" && AppConstants.platform == "win";
+ if (doCaseInsensitive) {
+ docPath = docPath.toLowerCase();
+ }
+
+ // Get document filename before we start chopping up the docPath
+ var docFilename = GetFilename(docPath);
+
+ // Both url and doc paths now begin with "/"
+ // Look for shared dirs starting after that
+ urlPath = urlPath.slice(1);
+ docPath = docPath.slice(1);
+
+ var firstDirTest = true;
+ var nextDocSlash = 0;
+ var done = false;
+
+ // Remove all matching subdirs common to both doc and input urls
+ do {
+ nextDocSlash = docPath.indexOf("/");
+ var nextUrlSlash = urlPath.indexOf("/");
+
+ if (nextUrlSlash == -1) {
+ // We're done matching and all dirs in url
+ // what's left is the filename
+ done = true;
+
+ // Remove filename for named anchors in the same file
+ if (nextDocSlash == -1 && docFilename) {
+ var anchorIndex = urlPath.indexOf("#");
+ if (anchorIndex > 0) {
+ var urlFilename = doCaseInsensitive ? urlPath.toLowerCase() : urlPath;
+
+ if (urlFilename.startsWith(docFilename)) {
+ urlPath = urlPath.slice(anchorIndex);
+ }
+ }
+ }
+ } else if (nextDocSlash >= 0) {
+ // Test for matching subdir
+ var docDir = docPath.slice(0, nextDocSlash);
+ var urlDir = urlPath.slice(0, nextUrlSlash);
+ if (doCaseInsensitive) {
+ urlDir = urlDir.toLowerCase();
+ }
+
+ if (urlDir == docDir) {
+ // Remove matching dir+"/" from each path
+ // and continue to next dir.
+ docPath = docPath.slice(nextDocSlash + 1);
+ urlPath = urlPath.slice(nextUrlSlash + 1);
+ } else {
+ // No match, we're done.
+ done = true;
+
+ // Be sure we are on the same local drive or volume
+ // (the first "dir" in the path) because we can't
+ // relativize to different drives/volumes.
+ // UNIX doesn't have volumes, so we must not do this else
+ // the first directory will be misinterpreted as a volume name.
+ if (
+ firstDirTest &&
+ docScheme == "file" &&
+ AppConstants.platform != "unix"
+ ) {
+ return inputUrl;
+ }
+ }
+ } else {
+ // No more doc dirs left, we're done
+ done = true;
+ }
+
+ firstDirTest = false;
+ } while (!done);
+
+ // Add "../" for each dir left in docPath
+ while (nextDocSlash > 0) {
+ urlPath = "../" + urlPath;
+ nextDocSlash = docPath.indexOf("/", nextDocSlash + 1);
+ }
+ return urlPath;
+}
+
+function MakeAbsoluteUrl(url) {
+ let resultUrl = TrimString(url);
+ if (!resultUrl) {
+ return resultUrl;
+ }
+
+ // Check if URL is already absolute, i.e., it has a scheme
+ let urlScheme = GetScheme(resultUrl);
+
+ if (urlScheme) {
+ return resultUrl;
+ }
+
+ let docUrl = GetDocumentBaseUrl();
+ let docScheme = GetScheme(docUrl);
+
+ // Can't relativize if no doc scheme (page hasn't been saved)
+ if (!docScheme) {
+ return resultUrl;
+ }
+
+ // Make a URI object to use its "resolve" method
+ let absoluteUrl = resultUrl;
+ let docUri = Services.io.newURI(
+ docUrl,
+ GetCurrentEditor().documentCharacterSet
+ );
+
+ try {
+ absoluteUrl = docUri.resolve(resultUrl);
+ // This is deprecated and buggy!
+ // If used, we must make it a path for the parent directory (remove filename)
+ // absoluteUrl = IOService.resolveRelativePath(resultUrl, docUrl);
+ } catch (e) {}
+
+ return absoluteUrl;
+}
+
+// Get the HREF of the page's <base> tag or the document location
+// returns empty string if no base href and document hasn't been saved yet
+function GetDocumentBaseUrl() {
+ try {
+ var docUrl;
+
+ // if document supplies a <base> tag, use that URL instead
+ let base = GetCurrentEditor().document.querySelector("base");
+ if (base) {
+ docUrl = base.getAttribute("href");
+ }
+ if (!docUrl) {
+ docUrl = GetDocumentUrl();
+ }
+
+ if (!IsUrlAboutBlank(docUrl)) {
+ return docUrl;
+ }
+ } catch (e) {}
+ return "";
+}
+
+function GetDocumentUrl() {
+ try {
+ return GetCurrentEditor().document.URL;
+ } catch (e) {}
+ return "";
+}
+
+// Extract the scheme (e.g., 'file', 'http') from a URL string
+function GetScheme(urlspec) {
+ var resultUrl = TrimString(urlspec);
+ // Unsaved document URL has no acceptable scheme yet
+ if (!resultUrl || IsUrlAboutBlank(resultUrl)) {
+ return "";
+ }
+
+ var scheme = "";
+ try {
+ // This fails if there's no scheme
+ scheme = Services.io.extractScheme(resultUrl);
+ } catch (e) {}
+
+ return scheme ? scheme.toLowerCase() : "";
+}
+
+function GetHost(urlspec) {
+ if (!urlspec) {
+ return "";
+ }
+
+ var host = "";
+ try {
+ host = Services.io.newURI(urlspec).host;
+ } catch (e) {}
+
+ return host;
+}
+
+function GetUsername(urlspec) {
+ if (!urlspec) {
+ return "";
+ }
+
+ var username = "";
+ try {
+ username = Services.io.newURI(urlspec).username;
+ } catch (e) {}
+
+ return username;
+}
+
+function GetFilename(urlspec) {
+ if (!urlspec || IsUrlAboutBlank(urlspec)) {
+ return "";
+ }
+
+ var filename;
+
+ try {
+ let uri = Services.io.newURI(urlspec);
+ if (uri) {
+ let url = uri.QueryInterface(Ci.nsIURL);
+ if (url) {
+ filename = url.fileName;
+ }
+ }
+ } catch (e) {}
+
+ return filename ? filename : "";
+}
+
+// Return the url without username and password
+// Optional output objects return extracted username and password strings
+// This uses just string routines via nsIIOServices
+function StripUsernamePassword(urlspec, usernameObj, passwordObj) {
+ urlspec = TrimString(urlspec);
+ if (!urlspec || IsUrlAboutBlank(urlspec)) {
+ return urlspec;
+ }
+
+ if (usernameObj) {
+ usernameObj.value = "";
+ }
+ if (passwordObj) {
+ passwordObj.value = "";
+ }
+
+ // "@" must exist else we will never detect username or password
+ var atIndex = urlspec.indexOf("@");
+ if (atIndex > 0) {
+ try {
+ let uri = Services.io.newURI(urlspec);
+ let username = uri.username;
+ let password = uri.password;
+
+ if (usernameObj && username) {
+ usernameObj.value = username;
+ }
+ if (passwordObj && password) {
+ passwordObj.value = password;
+ }
+ if (username) {
+ let usernameStart = urlspec.indexOf(username);
+ if (usernameStart != -1) {
+ return urlspec.slice(0, usernameStart) + urlspec.slice(atIndex + 1);
+ }
+ }
+ } catch (e) {}
+ }
+ return urlspec;
+}
+
+function StripPassword(urlspec, passwordObj) {
+ urlspec = TrimString(urlspec);
+ if (!urlspec || IsUrlAboutBlank(urlspec)) {
+ return urlspec;
+ }
+
+ if (passwordObj) {
+ passwordObj.value = "";
+ }
+
+ // "@" must exist else we will never detect password
+ var atIndex = urlspec.indexOf("@");
+ if (atIndex > 0) {
+ try {
+ let password = Services.io.newURI(urlspec).password;
+
+ if (passwordObj && password) {
+ passwordObj.value = password;
+ }
+ if (password) {
+ // Find last ":" before "@"
+ let colon = urlspec.lastIndexOf(":", atIndex);
+ if (colon != -1) {
+ // Include the "@"
+ return urlspec.slice(0, colon) + urlspec.slice(atIndex);
+ }
+ }
+ } catch (e) {}
+ }
+ return urlspec;
+}
+
+// Version to use when you have an nsIURI object
+function StripUsernamePasswordFromURI(uri) {
+ var urlspec = "";
+ if (uri) {
+ try {
+ urlspec = uri.spec;
+ var userPass = uri.userPass;
+ if (userPass) {
+ let start = urlspec.indexOf(userPass);
+ urlspec =
+ urlspec.slice(0, start) + urlspec.slice(start + userPass.length + 1);
+ }
+ } catch (e) {}
+ }
+ return urlspec;
+}
+
+function InsertUsernameIntoUrl(urlspec, username) {
+ if (!urlspec || !username) {
+ return urlspec;
+ }
+
+ try {
+ let URI = Services.io.newURI(
+ urlspec,
+ GetCurrentEditor().documentCharacterSet
+ );
+ URI.username = username;
+ return URI.spec;
+ } catch (e) {}
+
+ return urlspec;
+}
+
+function ConvertRGBColorIntoHEXColor(color) {
+ if (/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.test(color)) {
+ var r = Number(RegExp.$1).toString(16);
+ if (r.length == 1) {
+ r = "0" + r;
+ }
+ var g = Number(RegExp.$2).toString(16);
+ if (g.length == 1) {
+ g = "0" + g;
+ }
+ var b = Number(RegExp.$3).toString(16);
+ if (b.length == 1) {
+ b = "0" + b;
+ }
+ return "#" + r + g + b;
+ }
+
+ return color;
+}
+
+/** *********** CSS */
+
+function GetHTMLOrCSSStyleValue(element, attrName, cssPropertyName) {
+ var value;
+ if (Services.prefs.getBoolPref("editor.use_css") && IsHTMLEditor()) {
+ value = element.style.getPropertyValue(cssPropertyName);
+ }
+
+ if (!value) {
+ value = element.getAttribute(attrName);
+ }
+
+ if (!value) {
+ return "";
+ }
+
+ return value;
+}
+
+/** *********** Miscellaneous */
+// Clone simple JS objects
+function Clone(obj) {
+ var clone = {};
+ for (var i in obj) {
+ if (typeof obj[i] == "object") {
+ clone[i] = Clone(obj[i]);
+ } else {
+ clone[i] = obj[i];
+ }
+ }
+ return clone;
+}
+
+/**
+ * Utility functions to handle shortended data: URLs in EdColorProps.js and EdImageOverlay.js.
+ */
+
+/**
+ * Is the passed in image URI a shortened data URI?
+ *
+ * @returns {bool}
+ */
+function isImageDataShortened(aImageData) {
+ return /^data:/i.test(aImageData) && aImageData.includes("…");
+}
+
+/**
+ * Event handler for Copy or Cut
+ *
+ * @param aEvent the event
+ */
+function onCopyOrCutShortened(aEvent) {
+ // Put the original data URI onto the clipboard in case the value
+ // is a shortened data URI.
+ let field = aEvent.target;
+ let startPos = field.selectionStart;
+ if (startPos == undefined) {
+ return;
+ }
+ let endPos = field.selectionEnd;
+ let selection = field.value.substring(startPos, endPos).trim();
+
+ // Test that a) the user selected the whole value,
+ // b) the value is a data URI,
+ // c) it contains the ellipsis we added. Otherwise it could be
+ // a new value that the user pasted in.
+ if (selection == field.value.trim() && isImageDataShortened(selection)) {
+ aEvent.clipboardData.setData("text/plain", field.fullDataURI);
+ if (aEvent.type == "cut") {
+ // We have to cut the selection manually. Since we tested that
+ // everything was selected, we can just reset the field.
+ field.value = "";
+ }
+ aEvent.preventDefault();
+ }
+}
+
+/**
+ * Set up element showing an image URI with a shortened version.
+ * and add event handler for Copy or Cut.
+ *
+ * @param aImageData the data: URL of the image to be shortened.
+ * Note: Original stored in 'aDialogField.fullDataURI'.
+ * @param aDialogField The field of the dialog to contain the data.
+ * @returns {bool} URL was shortened?
+ */
+function shortenImageData(aImageData, aDialogField) {
+ let shortened = false;
+ aDialogField.value = aImageData.replace(
+ /^(data:.+;base64,)(.*)/i,
+ function (match, nonDataPart, dataPart) {
+ if (dataPart.length <= 35) {
+ return match;
+ }
+
+ shortened = true;
+ aDialogField.addEventListener("copy", onCopyOrCutShortened);
+ aDialogField.addEventListener("cut", onCopyOrCutShortened);
+ aDialogField.fullDataURI = aImageData;
+ aDialogField.removeAttribute("tooltiptext");
+ aDialogField.setAttribute("tooltip", "shortenedDataURI");
+ return (
+ nonDataPart +
+ dataPart.substring(0, 5) +
+ "…" +
+ dataPart.substring(dataPart.length - 30)
+ );
+ }
+ );
+ return shortened;
+}
+
+/**
+ * Return full data URIs for a shortened element.
+ *
+ * @param aDialogField The field of the dialog containing the data.
+ */
+function restoredImageData(aDialogField) {
+ return aDialogField.fullDataURI;
+}
diff --git a/comm/mail/components/compose/content/images/tag-anchor.gif b/comm/mail/components/compose/content/images/tag-anchor.gif
new file mode 100644
index 0000000000..ccb809b50b
--- /dev/null
+++ b/comm/mail/components/compose/content/images/tag-anchor.gif
Binary files differ
diff --git a/comm/mail/components/compose/content/messengercompose.xhtml b/comm/mail/components/compose/content/messengercompose.xhtml
new file mode 100644
index 0000000000..6881bf7cf3
--- /dev/null
+++ b/comm/mail/components/compose/content/messengercompose.xhtml
@@ -0,0 +1,2572 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messengercompose/messengercompose.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/attachmentList.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/menulist.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/inContentDialog.css" type="text/css"?>
+
+<!DOCTYPE html [
+ <!ENTITY % messengercomposeDTD SYSTEM "chrome://messenger/locale/messengercompose/messengercompose.dtd" >
+ %messengercomposeDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % customizeToolbarDTD SYSTEM "chrome://messenger/locale/customizeToolbar.dtd">
+ %customizeToolbarDTD;
+ <!ENTITY % viewZoomOverlayDTD SYSTEM "chrome://messenger/locale/viewZoomOverlay.dtd">
+ %viewZoomOverlayDTD;
+ <!ENTITY % baseMenuOverlayDTD SYSTEM "chrome://messenger/locale/baseMenuOverlay.dtd">
+ %baseMenuOverlayDTD;
+ <!ENTITY % msgCompSMIMEDTD SYSTEM "chrome://messenger-smime/locale/msgCompSMIMEOverlay.dtd">
+ %msgCompSMIMEDTD;
+ <!ENTITY % editorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/editorOverlay.dtd">
+ %editorOverlayDTD;
+ <!ENTITY % utilityOverlayDTD SYSTEM
+ "chrome://communicator/locale/utilityOverlay.dtd">
+ %utilityOverlayDTD;
+ <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
+ %messengerDTD;
+]>
+
+<html id="msgcomposeWindow" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ icon="msgcomposeWindow"
+ scrolling="false"
+ windowtype="msgcompose"
+ toggletoolbar="true"
+ persist="screenX screenY width height sizemode"
+ lightweightthemes="true"
+#ifdef XP_MACOSX
+ macanimationtype="document"
+ chromemargin="0,-1,-1,-1"
+#endif
+ fullscreenbutton="true">
+<head>
+ <title>&msgComposeWindow.title;</title>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="messenger/messenger.ftl" />
+ <link rel="localization" href="messenger/messengercompose/messengercompose.ftl" />
+ <link rel="localization" href="messenger/menubar.ftl" />
+ <link rel="localization" href="messenger/appmenu.ftl" />
+ <link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ <link rel="localization" href="messenger/openpgp/keyAssistant.ftl" />
+ <link rel="localization" href="messenger/openpgp/composeKeyStatus.ftl"/>
+ <link rel="localization" href="toolkit/main-window/findbar.ftl" />
+ <link rel="localization" href="toolkit/global/textActions.ftl" />
+ <link rel="localization" href="toolkit/printing/printUI.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/pane-splitter.js"></script>
+ <script defer="defer" src="chrome://messenger/content/accountUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailCore.js"></script>
+ <script defer="defer" src="chrome://communicator/content/contentAreaClick.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/editor.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/editorUtilities.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/ComposerCommands.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/MsgComposeCommands.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/bigFileObserver.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/cloudAttachmentLinkManager.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messenger-customization.js"></script>
+ <script defer="defer" src="chrome://messenger/content/customizable-toolbar.js"></script>
+ <script defer="defer" src="chrome://messenger/content/browserPopups.js"></script>
+ <script defer="defer" src="chrome://messenger/content/addressbook/abDragDrop.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/contentAreaUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/addressbook/abCommon.js"></script>
+ <script defer="defer" src="chrome://messenger/content/viewZoomOverlay.js"></script>
+#ifdef XP_MACOSX
+ <script defer="defer" src="chrome://global/content/macWindowMenu.js"></script>
+#endif
+ <script defer="defer" src="chrome://communicator/content/utilityOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/toolbarIconColor.js"></script>
+ <script defer="defer" src="chrome://openpgp/content/ui/enigmailMsgComposeOverlay.js"></script>
+ <script defer="defer" src="chrome://openpgp/content/ui/commonWorkflows.js"></script>
+ <script defer="defer" src="chrome://openpgp/content/ui/keyAssistant.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <stringbundle id="bundle_composeMsgs" src="chrome://messenger/locale/messengercompose/composeMsgs.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="brandBundle" src="chrome://branding/locale/brand.properties"/>
+
+<commandset id="composeCommands">
+ <commandset id="msgComposeCommandUpdate"
+ commandupdater="true"
+ events="focus"
+ oncommandupdate="CommandUpdate_MsgCompose()"/>
+ <commandset id="globalEditMenuItems"
+ commandupdater="true"
+ events="focus"
+ oncommandupdate="goUpdateGlobalEditMenuItems()"/>
+ <commandset id="selectEditMenuItems"
+ commandupdater="true"
+ events="select"
+ oncommandupdate="goUpdateSelectEditMenuItems()"/>
+ <commandset id="undoEditMenuItems"
+ commandupdater="true"
+ events="undo"
+ oncommandupdate="goUpdateUndoEditMenuItems()"/>
+ <commandset id="clipboardEditMenuItems"
+ commandupdater="true"
+ events="clipboard"
+ oncommandupdate="goUpdatePasteMenuItems()"/>
+
+ <!-- commands updated when the editor gets created -->
+ <commandset id="commonEditorMenuItems"
+ commandupdater="true"
+ events="create"
+ oncommandupdate="goUpdateComposerMenuItems(this)">
+ <command id="cmd_print" oncommand="goDoCommand('cmd_print')"/>
+ <command id="cmd_quitApplication" oncommand="goDoCommand('cmd_quitApplication')"/>
+ </commandset>
+
+ <commandset id="composerMenuItems"
+ commandupdater="true"
+ events="create, mode_switch"
+ oncommandupdate="goUpdateComposerMenuItems(this)">
+ <!-- format menu -->
+ <command id="cmd_listProperties" oncommand="goDoCommand('cmd_listProperties')"/>
+ <command id="cmd_colorProperties" oncommand="goDoCommand('cmd_colorProperties')"/>
+
+ <command id="cmd_link" oncommand="goDoCommand('cmd_link')"/>
+ <command id="cmd_anchor" oncommand="goDoCommand('cmd_anchor')"/>
+ <command id="cmd_image" oncommand="goDoCommand('cmd_image')"/>
+ <command id="cmd_hline" oncommand="goDoCommand('cmd_hline')"/>
+ <command id="cmd_table" oncommand="goDoCommand('cmd_table')"/>
+ <command id="cmd_objectProperties" oncommand="goDoCommand('cmd_objectProperties')"/>
+ <command id="cmd_insertChars" oncommand="goDoCommand('cmd_insertChars')" label="&insertCharsCmd.label;"/>
+ <command id="cmd_insertHTMLWithDialog" oncommand="goDoCommand('cmd_insertHTMLWithDialog')" label="&insertHTMLCmd.label;"/>
+ <command id="cmd_insertMathWithDialog" oncommand="goDoCommand('cmd_insertMathWithDialog')" label="&insertMathCmd.label;"/>
+
+ <command id="cmd_insertBreakAll" oncommand="goDoCommand('cmd_insertBreakAll')"/>
+
+ <!-- dummy command used just to disable things in non-HTML modes -->
+ <command id="cmd_renderedHTMLEnabler"/>
+ </commandset>
+
+ <!-- edit menu commands. These get updated by code in globalOverlay.js -->
+ <commandset id="composerEditMenuItems"
+ commandupdater="true"
+ events="create, mode_switch"
+ oncommandupdate="goUpdateComposerMenuItems(this)">
+ <command id="cmd_pasteNoFormatting" oncommand="goDoCommand('cmd_pasteNoFormatting')"
+ label="&pasteNoFormatting.label;" accesskey="&pasteNoFormatting.accesskey;"/>
+ <command id="cmd_findReplace" oncommand="goDoCommand('cmd_findReplace')"/>
+ <command id="cmd_find" oncommand="goDoCommand('cmd_find')"/>
+ <command id="cmd_findNext" oncommand="goDoCommand('cmd_findNext');"/>
+ <command id="cmd_findPrev" oncommand="goDoCommand('cmd_findPrev');"/>
+ <command id="cmd_spelling" oncommand="goDoCommand('cmd_spelling')"/>
+ <command id="cmd_pasteQuote" oncommand="goDoCommand('cmd_pasteQuote')" label="&pasteAsQuotationCmd.label;"/>
+ </commandset>
+
+ <!-- style related commands that update on creation, and on selection change -->
+ <commandset id="composerStyleMenuItems"
+ commandupdater="true"
+ events="create, style, mode_switch"
+ oncommandupdate="goUpdateComposerMenuItems(this)">
+ <command id="cmd_bold" state="false" oncommand="doStyleUICommand('cmd_bold')"/>
+ <command id="cmd_italic" state="false" oncommand="doStyleUICommand('cmd_italic')"/>
+ <command id="cmd_underline" state="false" oncommand="doStyleUICommand('cmd_underline')"/>
+ <command id="cmd_tt" state="false" oncommand="goDoCommand('cmd_tt')"/>
+ <command id="cmd_smiley"/>
+
+ <command id="cmd_strikethrough" state="false" oncommand="doStyleUICommand('cmd_strikethrough');"/>
+ <command id="cmd_superscript" state="false" oncommand="doStyleUICommand('cmd_superscript');"/>
+ <command id="cmd_subscript" state="false" oncommand="doStyleUICommand('cmd_subscript');"/>
+ <command id="cmd_nobreak" state="false" oncommand="goDoCommand('cmd_nobreak');"/>
+
+ <command id="cmd_em" state="false" oncommand="goDoCommand('cmd_em')"/>
+ <command id="cmd_strong" state="false" oncommand="goDoCommand('cmd_strong')"/>
+ <command id="cmd_cite" state="false" oncommand="goDoCommand('cmd_cite')"/>
+ <command id="cmd_abbr" state="false" oncommand="goDoCommand('cmd_abbr')"/>
+ <command id="cmd_acronym" state="false" oncommand="goDoCommand('cmd_acronym')"/>
+ <command id="cmd_code" state="false" oncommand="goDoCommand('cmd_code')"/>
+ <command id="cmd_samp" state="false" oncommand="goDoCommand('cmd_samp')"/>
+ <command id="cmd_var" state="false" oncommand="goDoCommand('cmd_var')"/>
+
+ <command id="cmd_ul" state="false" oncommand="doStyleUICommand('cmd_ul')"/>
+ <command id="cmd_ol" state="false" oncommand="doStyleUICommand('cmd_ol')"/>
+
+ <command id="cmd_indent" oncommand="goDoCommand('cmd_indent')"/>
+ <command id="cmd_outdent" oncommand="goDoCommand('cmd_outdent')"/>
+
+ <command id="cmd_paragraphState" state=""/>
+ <command id="cmd_fontFace" state="" oncommand="doStatefulCommand('cmd_fontFace', event.target.value)"/>
+
+ <!-- No "oncommand", use EditorSelectColor() to bring up color dialog -->
+ <command id="cmd_fontColor" state="" disabled="false"/>
+ <command id="cmd_backgroundColor" state="" disabled="false"/>
+ <command id="cmd_highlight" state="transparent" oncommand="EditorSelectColor('Highlight', event);"/>
+
+ <command id="cmd_align" state=""/>
+
+ <command id="cmd_increaseFontStep" oncommand="goDoCommand('cmd_increaseFontStep')"/>
+ <command id="cmd_decreaseFontStep" oncommand="goDoCommand('cmd_decreaseFontStep')"/>
+
+ <command id="cmd_removeStyles" oncommand="editorRemoveTextStyling();"/>
+ <command id="cmd_removeLinks" oncommand="goDoCommand('cmd_removeLinks')"/>
+ <command id="cmd_removeNamedAnchors" oncommand="goDoCommand('cmd_removeNamedAnchors')"/>
+ </commandset>
+
+ <commandset id="composerTableMenuItems"
+ commandupdater="true"
+ events="create, mode_switch"
+ oncommandupdate="goUpdateTableMenuItems(this)">
+ <!-- Table menu -->
+ <command id="cmd_SelectTable" oncommand="goDoCommand('cmd_SelectTable')"/>
+ <command id="cmd_SelectRow" oncommand="goDoCommand('cmd_SelectRow')"/>
+ <command id="cmd_SelectColumn" oncommand="goDoCommand('cmd_SelectColumn')"/>
+ <command id="cmd_SelectCell" oncommand="goDoCommand('cmd_SelectCell')"/>
+ <command id="cmd_SelectAllCells" oncommand="goDoCommand('cmd_SelectAllCells')"/>
+ <command id="cmd_InsertTable" oncommand="goDoCommand('cmd_InsertTable')"/>
+ <command id="cmd_InsertRowAbove" oncommand="goDoCommand('cmd_InsertRowAbove')"/>
+ <command id="cmd_InsertRowBelow" oncommand="goDoCommand('cmd_InsertRowBelow')"/>
+ <command id="cmd_InsertColumnBefore" oncommand="goDoCommand('cmd_InsertColumnBefore')"/>
+ <command id="cmd_InsertColumnAfter" oncommand="goDoCommand('cmd_InsertColumnAfter')"/>
+ <command id="cmd_InsertCellBefore" oncommand="goDoCommand('cmd_InsertCellBefore')"/>
+ <command id="cmd_InsertCellAfter" oncommand="goDoCommand('cmd_InsertCellAfter')"/>
+ <command id="cmd_DeleteTable" oncommand="goDoCommand('cmd_DeleteTable')"/>
+ <command id="cmd_DeleteRow" oncommand="goDoCommand('cmd_DeleteRow')"/>
+ <command id="cmd_DeleteColumn" oncommand="goDoCommand('cmd_DeleteColumn')"/>
+ <command id="cmd_DeleteCell" oncommand="goDoCommand('cmd_DeleteCell')"/>
+ <command id="cmd_DeleteCellContents" oncommand="goDoCommand('cmd_DeleteCellContents')"/>
+ <command id="cmd_JoinTableCells" oncommand="goDoCommand('cmd_JoinTableCells')"/>
+ <command id="cmd_SplitTableCell" oncommand="goDoCommand('cmd_SplitTableCell')"/>
+ <command id="cmd_ConvertToTable" oncommand="goDoCommand('cmd_ConvertToTable')"/>
+ <command id="cmd_TableOrCellColor" oncommand="goDoCommand('cmd_TableOrCellColor')"/>
+ <command id="cmd_editTable" oncommand="goDoCommand('cmd_editTable')"/>
+ </commandset>
+
+ <!-- commands updated only when the menu gets created -->
+ <commandset id="composerListMenuItems"
+ commandupdater="true"
+ events="create, mode_switch"
+ oncommandupdate="goUpdateComposerMenuItems(this)">
+ <!-- List menu -->
+ <command id="cmd_dt" oncommand="goDoCommand('cmd_dt')"/>
+ <command id="cmd_dd" oncommand="goDoCommand('cmd_dd')"/>
+ <command id="cmd_removeList" oncommand="goDoCommand('cmd_removeList')"/>
+ <!-- cmd_ul and cmd_ol are shared with toolbar and are in composerStyleMenuItems commandset -->
+ </commandset>
+
+ <!-- File Menu -->
+ <command id="cmd_new" oncommand="goDoCommand('cmd_newMessage')"/>
+ <command id="cmd_attachFile" oncommand="goDoCommand('cmd_attachFile')"/>
+ <command id="cmd_attachCloud" oncommand="attachToCloud(event)"/>
+ <command id="cmd_attachPage" oncommand="goDoCommand('cmd_attachPage')"/>
+ <command id="cmd_attachVCard" checked="false"
+ oncommand="ToggleAttachVCard(event.target)"/>
+ <command id="cmd_attachPublicKey" checked="false"
+ oncommand="toggleAttachMyPublicKey(event.target)"/>
+ <command id="cmd_remindLater" checked="false"
+ oncommand="toggleAttachmentReminder()"/>
+ <command id="cmd_close" oncommand="goDoCommand('cmd_close')"/>
+ <command id="cmd_saveDefault" oncommand="goDoCommand('cmd_saveDefault')"/>
+ <command id="cmd_saveAsFile" oncommand="goDoCommand('cmd_saveAsFile')"/>
+ <command id="cmd_saveAsDraft" oncommand="goDoCommand('cmd_saveAsDraft')"/>
+ <command id="cmd_saveAsTemplate" oncommand="goDoCommand('cmd_saveAsTemplate')"/>
+ <command id="cmd_sendButton" oncommand="goDoCommand('cmd_sendButton')"/>
+ <command id="cmd_sendNow" oncommand="goDoCommand('cmd_sendNow')"/>
+ <command id="cmd_sendWithCheck" oncommand="goDoCommand('cmd_sendWithCheck')"/>
+ <command id="cmd_sendLater" oncommand="goDoCommand('cmd_sendLater')"/>
+ <command id="cmd_print" oncommand="goDoCommand('cmd_print')"/>
+
+ <!-- Edit Menu -->
+ <!--command id="cmd_pasteQuote"/ DO NOT INCLUDE THOSE COMMANDS ELSE THE EDIT MENU WILL BE BROKEN! -->
+ <!--command id="cmd_find"/-->
+ <!--command id="cmd_findNext"/-->
+ <command id="cmd_undo" oncommand="goDoCommand('cmd_undo')" disabled="true"/>
+ <command id="cmd_redo" oncommand="goDoCommand('cmd_redo')" disabled="true"/>
+ <command id="cmd_cut" oncommand="goDoCommand('cmd_cut')" disabled="true"/>
+ <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')" disabled="true"/>
+ <command id="cmd_paste" oncommand="goDoCommand('cmd_paste')" disabled="true"/>
+ <command id="cmd_rewrap" oncommand="goDoCommand('cmd_rewrap')"/>
+ <command id="cmd_delete"
+ oncommand="goDoCommand('cmd_delete')"
+ valueDefault="&deleteCmd.label;"
+ valueDefaultAccessKey="&deleteCmd.accesskey;"
+ valueRemoveAttachmentAccessKey="&removeAttachment.accesskey;"
+ disabled="true"/>
+ <command id="cmd_selectAll"
+ oncommand="goDoCommand('cmd_selectAll')" disabled="true"/>
+ <command id="cmd_removeAllAttachments"
+ oncommand="goDoCommand('cmd_removeAllAttachments')"/>
+ <command id="cmd_openAttachment"
+ oncommand="goDoCommand('cmd_openAttachment')" disabled="true"/>
+ <command id="cmd_renameAttachment"
+ oncommand="goDoCommand('cmd_renameAttachment')" disabled="true"/>
+ <command id="cmd_reorderAttachments"
+ oncommand="goDoCommand('cmd_reorderAttachments')" disabled="true"/>
+ <command id="cmd_toggleAttachmentPane"
+ oncommand="goDoCommand('cmd_toggleAttachmentPane')"/>
+ <command id="cmd_account"
+ oncommand="goDoCommand('cmd_account')"/>
+
+ <!-- Reorder Attachments Panel -->
+ <command id="cmd_moveAttachmentLeft"
+ oncommand="goDoCommand('cmd_moveAttachmentLeft')" disabled="true"/>
+ <command id="cmd_moveAttachmentRight"
+ oncommand="goDoCommand('cmd_moveAttachmentRight')" disabled="true"/>
+ <command id="cmd_moveAttachmentBundleUp"
+ oncommand="goDoCommand('cmd_moveAttachmentBundleUp')" disabled="true"/>
+ <command id="cmd_moveAttachmentBundleDown"
+ oncommand="goDoCommand('cmd_moveAttachmentBundleDown')" disabled="true"/>
+ <command id="cmd_moveAttachmentTop"
+ oncommand="goDoCommand('cmd_moveAttachmentTop')" disabled="true"/>
+ <command id="cmd_moveAttachmentBottom"
+ oncommand="goDoCommand('cmd_moveAttachmentBottom')" disabled="true"/>
+ <command id="cmd_sortAttachmentsToggle"
+ sortdirection="ascending"
+ oncommand="goDoCommand('cmd_sortAttachmentsToggle')" disabled="true"/>
+
+ <!-- View Menu -->
+ <command id="cmd_showFormatToolbar"
+ oncommand="goDoCommand('cmd_showFormatToolbar')"/>
+
+ <commandset id="viewZoomCommands"
+ commandupdater="false"
+ events="create-menu-view"
+ oncommandupdate="goUpdateMailMenuItems(this);">
+ <command id="cmd_fullZoomReduce"
+ oncommand="goDoCommand('cmd_fullZoomReduce');"/>
+ <command id="cmd_fullZoomEnlarge"
+ oncommand="goDoCommand('cmd_fullZoomEnlarge');"/>
+ <command id="cmd_fullZoomReset"
+ oncommand="goDoCommand('cmd_fullZoomReset');"/>
+ <command id="cmd_fullZoomToggle"
+ oncommand="goDoCommand('cmd_fullZoomToggle');"/>
+ </commandset>
+
+ <!-- Options Menu -->
+ <command id="cmd_quoteMessage" oncommand="goDoCommand('cmd_quoteMessage')"/>
+ <command id="cmd_toggleReturnReceipt"
+ oncommand="goDoCommand('cmd_toggleReturnReceipt')"/>
+ <command id="cmd_insert"/>
+ <command id="cmd_viewSecurityStatus"
+ oncommand="showMessageComposeSecurityStatus();"/>
+
+#ifdef XP_MACOSX
+ <!-- Mac Window menu -->
+ <command id="minimizeWindow" label="&minimizeWindow.label;" oncommand="window.minimize();"/>
+ <command id="zoomWindow" label="&zoomWindow.label;" oncommand="zoomWindow();"/>
+#endif
+
+ <command id="cmd_CustomizeComposeToolbar"
+ oncommand="CustomizeMailToolbar('compose-toolbox', 'CustomizeComposeToolbar')"/>
+
+ <command id="cmd_convertCloud" oncommand="convertSelectedToCloudAttachment(event.target.cloudFileAccount); event.stopPropagation();"/>
+ <command id="cmd_convertAttachment" oncommand="goDoCommand('cmd_convertAttachment')"/>
+ <command id="cmd_cancelUpload" oncommand="goDoCommand('cmd_cancelUpload')"/>
+ <command id="cmd_customizeFromAddress" oncommand="MakeFromFieldEditable();"
+ checked="false" label="&customizeFromAddress.label;"/>
+</commandset>
+
+ <commandset>
+ <command id="cmd_reload" oncommand="document.getElementById('requestFrame').reload()"/>
+ <command id="cmd_stop" oncommand="document.getElementById('requestFrame').stop()"/>
+ <command id="cmd_copyLink" oncommand="goDoCommand('cmd_copyLink')" disabled="false"/>
+ <command id="cmd_copyImage" oncommand="goDoCommand('cmd_copyImageContents')" disabled="false"/>
+ </commandset>
+
+<keyset id="tasksKeys">
+ <!-- File Menu -->
+ <key id="key_newMessage" key="&newMessageCmd2.key;" oncommand="goOpenNewMessage(null);" modifiers="accel"/>
+ <key id="key_close" key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
+ <key id="key_save" key="&saveCmd.key;" command="cmd_saveDefault" modifiers="accel"/>
+ <key id="key_send" keycode="&sendCmd.keycode;" observes="cmd_sendWithCheck" modifiers="accel"/>
+ <key id="key_sendLater" keycode="&sendLaterCmd.keycode;" observes="cmd_sendLater" modifiers="accel, shift"/>
+ <key id="key_print" key="&printCmd.key;" command="cmd_print" modifiers="accel"/>
+ <key id="printKb" key="&printCmd.key;" command="cmd_print" modifiers="accel"/>
+
+ <!-- Edit Menu -->
+ <key id="key_undo" data-l10n-id="text-action-undo-shortcut" modifiers="accel" internal="true"/>
+ <key id="key_redo"
+#ifdef XP_UNIX
+ data-l10n-id="text-action-undo-shortcut"
+ modifiers="accel,shift"
+#else
+ data-l10n-id="text-action-redo-shortcut"
+ modifiers="accel"
+#endif
+ internal="true"/>
+ <key id="key_cut" data-l10n-id="text-action-cut-shortcut" modifiers="accel" internal="true"/>
+ <key id="key_copy" data-l10n-id="text-action-copy-shortcut" modifiers="accel" internal="true"/>
+ <key id="key_paste" data-l10n-id="text-action-paste-shortcut" modifiers="accel" internal="true"/>
+ <key id="pastequotationkb" key="&pasteAsQuotationCmd.key;"
+ observes="cmd_pasteQuote" modifiers="accel, shift"/>
+ <key id="pastenoformattingkb" key="&pasteNoFormattingCmd.key;"
+ modifiers="accel, shift" observes="cmd_pasteNoFormatting"/>
+ <key id="key_rewrap" key="&editRewrapCmd.key;" command="cmd_rewrap" modifiers="accel"/>
+#ifdef XP_MACOSX
+ <key id="key_delete" keycode="VK_BACK" command="cmd_delete"/>
+ <key id="key_delete2" keycode="VK_DELETE" command="cmd_delete"/>
+#else
+ <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/>
+ <key id="key_renameAttachment" keycode="VK_F2"
+ command="cmd_renameAttachment"/>
+#endif
+ <key id="key_reorderAttachments"
+ key="&reorderAttachmentsCmd.key;" modifiers="accel,shift"
+ command="cmd_reorderAttachments"/>
+ <key id="key_selectAll" data-l10n-id="text-action-select-all-shortcut" modifiers="accel" internal="true"/>
+ <key id="key_find" key="&findBarCmd.key;" command="cmd_find" modifiers="accel"/>
+#ifndef XP_MACOSX
+ <key id="key_findReplace" key="&findReplaceCmd.key;" command="cmd_findReplace" modifiers="accel"/>
+#endif
+ <key id="key_findNext" key="&findAgainCmd.key;" command="cmd_findNext" modifiers="accel"/>
+ <key id="key_findPrev" key="&findPrevCmd.key;" command="cmd_findPrev" modifiers="accel, shift"/>
+ <key keycode="&findAgainCmd.key2;" command="cmd_findNext"/>
+ <key keycode="&findPrevCmd.key2;" command="cmd_findPrev" modifiers="shift"/>
+
+ <!-- Reorder Attachments Panel -->
+ <key id="key_moveAttachmentLeft" keycode="VK_LEFT" modifiers="alt"
+ command="cmd_moveAttachmentLeft"/>
+ <key id="key_moveAttachmentRight" keycode="VK_RIGHT" modifiers="alt"
+ command="cmd_moveAttachmentRight"/>
+ <key id="key_moveAttachmentBundleUp" keycode="VK_UP" modifiers="alt"
+ command="cmd_moveAttachmentBundleUp"/>
+ <key id="key_moveAttachmentBundleDown" keycode="VK_DOWN" modifiers="alt"
+ command="cmd_moveAttachmentBundleDown"/>
+#ifdef XP_MACOSX
+ <key id="key_moveAttachmentTop" keycode="VK_UP" modifiers="accel alt"
+ command="cmd_moveAttachmentTop"/>
+ <key id="key_moveAttachmentBottom" keycode="VK_DOWN" modifiers="accel alt"
+ command="cmd_moveAttachmentBottom"/>
+ <key id="key_moveAttachmentTop2" keycode="VK_Home" modifiers="alt"
+ command="cmd_moveAttachmentTop"/>
+ <key id="key_moveAttachmentBottom2" keycode="VK_End" modifiers="alt"
+ command="cmd_moveAttachmentBottom"/>
+#else
+ <key id="key_moveAttachmentTop" keycode="VK_Home" modifiers="alt"
+ command="cmd_moveAttachmentTop"/>
+ <key id="key_moveAttachmentBottom" keycode="VK_End" modifiers="alt"
+ command="cmd_moveAttachmentBottom"/>
+#endif
+ <key id="key_sortAttachmentsToggle" key="&sortAttachmentsPanelBtn.key;"
+ modifiers="alt" command="cmd_sortAttachmentsToggle"/>
+
+ <!-- View Menu -->
+ <key id="key_addressSidebar" keycode="VK_F9" oncommand="toggleContactsSidebar();"/>
+
+ <keyset id="viewZoomKeys">
+ <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;"
+ command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key key="&fullZoomReduceCmd.commandkey2;"
+ command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;"
+ command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey2;"
+ command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey3;"
+ command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;"
+ command="cmd_fullZoomReset" modifiers="accel"/>
+ <key key="&fullZoomResetCmd.commandkey2;"
+ command="cmd_fullZoomReset" modifiers="accel"/>
+ </keyset>
+
+ <!-- Options Menu -->
+ <key id="key_checkspelling" key="&checkSpellingCmd2.key;" command="cmd_spelling" modifiers="accel,shift"/>
+
+#ifdef XP_WIN
+ <key keycode="&checkSpellingCmd2.key2;" command="cmd_spelling"/>
+#endif
+
+ <!-- Tools Menu -->
+ <key id="key_mail" key="&messengerCmd.commandkey;" oncommand="toMessengerWindow();" modifiers="accel"/>
+
+ <!-- Tab/F6 Keys -->
+ <key keycode="VK_TAB" oncommand="moveFocusToNeighbouringArea(event);" modifiers="control"/>
+ <key keycode="VK_TAB" oncommand="moveFocusToNeighbouringArea(event);" modifiers="control,shift"/>
+ <key keycode="VK_F6" oncommand="moveFocusToNeighbouringArea(event);" modifiers="control"/>
+ <key keycode="VK_F6" oncommand="moveFocusToNeighbouringArea(event);" modifiers="control,shift"/>
+ <key keycode="VK_F6" oncommand="moveFocusToNeighbouringArea(event);" modifiers="shift"/>
+ <key keycode="VK_F6" oncommand="moveFocusToNeighbouringArea(event);"/>
+
+#ifdef XP_MACOSX
+ <!-- Mac Window Menu -->
+ <key id="key_minimizeWindow" command="minimizeWindow" key="&minimizeWindow.key;" modifiers="accel"/>
+ <key id="key_openHelp" oncommand="openSupportURL();" key="&productHelpMac.commandkey;" modifiers="&productHelpMac.modifiers;"/>
+#else
+ <key id="key_openHelp" oncommand="openSupportURL();" keycode="&productHelp.commandkey;"/>
+#endif
+ <key keycode="VK_ESCAPE" oncommand="handleEsc();"/>
+</keyset>
+
+<keyset id="editorKeys">
+ <key id="boldkb" key="&styleBoldCmd.key;" observes="cmd_bold" modifiers="accel"/>
+ <key id="italickb" key="&styleItalicCmd.key;" observes="cmd_italic" modifiers="accel"/>
+ <key id="underlinekb" key="&styleUnderlineCmd.key;" observes="cmd_underline" modifiers="accel"/>
+ <key id="fixedwidthkb" key="&fontFixedWidth.key;" observes="cmd_tt" modifiers="accel"/>
+
+ <key id="increaseindentkb" key="&increaseIndent.key;" observes="cmd_indent" modifiers="accel"/>
+ <key id="decreaseindentkb" key="&decreaseIndent.key;" observes="cmd_outdent" modifiers="accel"/>
+
+ <key id="removestyleskb" key="&formatRemoveStyles.key;" observes="cmd_removeStyles" modifiers="accel, shift"/>
+ <key id="removestyleskb2" key=" " observes="cmd_removeStyles" modifiers="accel"/>
+ <key id="removelinkskb" key="&formatRemoveLinks.key;" observes="cmd_removeLinks" modifiers="accel, shift"/>
+ <key id="removenamedanchorskb" key="&formatRemoveNamedAnchors2.key;" observes="cmd_removeNamedAnchors" modifiers="accel, shift"/>
+ <key id="decreasefontsizekb" key="&decrementFontSize.key;" observes="cmd_decreaseFontStep" modifiers="accel"/>
+ <key key="&decrementFontSize.key;" observes="cmd_decreaseFontStep" modifiers="accel, shift"/>
+ <key key="&decrementFontSize.key2;" observes="cmd_decreaseFontStep" modifiers="accel"/>
+
+ <key id="increasefontsizekb" key="&incrementFontSize.key;" observes="cmd_increaseFontStep" modifiers="accel"/>
+ <key key="&incrementFontSize.key;" observes="cmd_increaseFontStep" modifiers="accel, shift"/>
+ <key key="&incrementFontSize.key2;" observes="cmd_increaseFontStep" modifiers="accel"/>
+
+ <key id="insertlinkkb" key="&insertLinkCmd2.key;" observes="cmd_link" modifiers="accel"/>
+</keyset>
+
+<popupset id="mainPopupSet">
+#include ../../../base/content/widgets/browserPopups.inc.xhtml
+</popupset>
+
+<!-- Reorder Attachments Panel -->
+<panel id="reorderAttachmentsPanel"
+ orient="vertical"
+ type="arrow"
+ flip="slide"
+ onpopupshowing="reorderAttachmentsPanelOnPopupShowing();"
+ consumeoutsideclicks="false"
+ noautohide="true">
+ <description class="panelTitle">&reorderAttachmentsPanel.label;</description>
+ <toolbarbutton id="btn_moveAttachmentFirst"
+ class="panelButton"
+ data-l10n-id="move-attachment-first-panel-button"
+ key="key_moveAttachmentTop"
+ command="cmd_moveAttachmentTop"/>
+ <toolbarbutton id="btn_moveAttachmentLeft"
+ class="panelButton"
+ data-l10n-id="move-attachment-left-panel-button"
+ key="key_moveAttachmentLeft"
+ command="cmd_moveAttachmentLeft"/>
+ <toolbarbutton id="btn_moveAttachmentBundleUp"
+ class="panelButton"
+ label="&moveAttachmentBundleUpPanelBtn.label;"
+ key="key_moveAttachmentBundleUp"
+ command="cmd_moveAttachmentBundleUp"/>
+ <toolbarbutton id="btn_moveAttachmentRight"
+ class="panelButton"
+ data-l10n-id="move-attachment-right-panel-button"
+ key="key_moveAttachmentRight"
+ command="cmd_moveAttachmentRight"/>
+ <toolbarbutton id="btn_moveAttachmentLast"
+ class="panelButton"
+ data-l10n-id="move-attachment-last-panel-button"
+ key="key_moveAttachmentBottom"
+ command="cmd_moveAttachmentBottom"/>
+ <toolbarbutton id="btn_sortAttachmentsToggle"
+ class="panelButton"
+ label="&sortAttachmentsPanelBtn.Sort.AZ.label;"
+ label-AZ="&sortAttachmentsPanelBtn.Sort.AZ.label;"
+ label-ZA="&sortAttachmentsPanelBtn.Sort.ZA.label;"
+ label-selection-AZ="&sortAttachmentsPanelBtn.SortSelection.AZ.label;"
+ label-selection-ZA="&sortAttachmentsPanelBtn.SortSelection.ZA.label;"
+ key="key_sortAttachmentsToggle"
+ command="cmd_sortAttachmentsToggle"/>
+</panel>
+
+<menupopup id="extraAddressRowsMenu"
+ class="no-icon-menupopup no-accel-menupopup"
+ onpopupshown="extraAddressRowsMenuOpened();"
+ onpopuphidden="extraAddressRowsMenuClosed();">
+ <!-- Default set up is for a mail account, where we prefer showing the
+ - buttons, rather than the menu items, for the mail rows.
+ - For the news rows, we prefer the menu items over the buttons. -->
+ <menuitem id="addr_replyShowAddressRowMenuItem"
+ class="menuitem-iconic"
+ oncommand="showAndFocusAddressRow('addressRowReply')"
+ label="&replyAddr2.label;"/>
+ <menuitem id="addr_toShowAddressRowMenuItem" disableonsend="true"
+ class="mail-show-row-menuitem menuitem-iconic"
+ oncommand="showAndFocusAddressRow('addressRowTo')"
+ hidden="true"
+ data-button-id="addr_toShowAddressRowButton"
+ data-prefer-button="true"/>
+ <menuitem id="addr_ccShowAddressRowMenuItem" disableonsend="true"
+ class="mail-show-row-menuitem menuitem-iconic"
+ oncommand="showAndFocusAddressRow('addressRowCc')"
+ hidden="true"
+ data-button-id="addr_ccShowAddressRowButton"
+ data-prefer-button="true"/>
+ <menuitem id="addr_bccShowAddressRowMenuItem" disableonsend="true"
+ class="mail-show-row-menuitem menuitem-iconic"
+ oncommand="showAndFocusAddressRow('addressRowBcc')"
+ hidden="true"
+ data-button-id="addr_bccShowAddressRowButton"
+ data-prefer-button="true"/>
+ <menuitem id="addr_newsgroupsShowAddressRowMenuItem"
+ class="news-show-row-menuitem menuitem-iconic"
+ oncommand="showAndFocusAddressRow('addressRowNewsgroups')"
+ data-button-id="addr_newsgroupsShowAddressRowButton"
+ label="&newsgroupsAddr2.label;"
+ data-prefer-button="false"/>
+ <menuitem id="addr_followupShowAddressRowMenuItem"
+ class="news-show-row-menuitem menuitem-iconic"
+ oncommand="showAndFocusAddressRow('addressRowFollowup')"
+ data-button-id="addr_followupShowAddressRowButton"
+ label="&followupAddr2.label;"
+ data-prefer-button="false"/>
+</menupopup>
+
+<menupopup id="msgComposeContext"
+ onpopupshowing="msgComposeContextOnShowing(event);"
+ onpopuphiding="msgComposeContextOnHiding(event);">
+
+ <!-- Spellchecking menu items -->
+ <menuitem id="spellCheckNoSuggestions"
+ data-l10n-id="text-action-spell-no-suggestions"
+ disabled="true"/>
+ <menuseparator id="spellCheckAddSep" />
+ <menuitem id="spellCheckAddToDictionary"
+ data-l10n-id="text-action-spell-add-to-dictionary"
+ oncommand="gSpellChecker.addToDictionary();"/>
+ <menuitem id="spellCheckUndoAddToDictionary"
+ data-l10n-id="text-action-spell-undo-add-to-dictionary"
+ oncommand="gSpellChecker.undoAddToDictionary();" />
+ <menuitem id="spellCheckIgnoreWord" label="&spellCheckIgnoreWord.label;"
+ accesskey="&spellCheckIgnoreWord.accesskey;"
+ oncommand="gSpellChecker.ignoreWord();"/>
+ <menuseparator id="spellCheckSuggestionsSeparator"/>
+
+ <menuitem data-l10n-id="text-action-undo" command="cmd_undo"/>
+ <menuitem data-l10n-id="text-action-cut" command="cmd_cut"/>
+ <menuitem data-l10n-id="text-action-copy" command="cmd_copy"/>
+ <menuitem data-l10n-id="text-action-paste" command="cmd_paste"/>
+ <menuitem command="cmd_pasteNoFormatting"/>
+ <menuitem label="&pasteQuote.label;" accesskey="&pasteQuote.accesskey;" command="cmd_pasteQuote"/>
+ <menuitem data-l10n-id="text-action-delete" command="cmd_delete"/>
+ <menuseparator/>
+ <menuitem data-l10n-id="text-action-select-all" command="cmd_selectAll"/>
+
+ <!-- Spellchecking general menu items (enable, add dictionaries...) -->
+ <menuseparator id="spellCheckSeparator"/>
+ <menuitem id="spellCheckEnable"
+ data-l10n-id="text-action-spell-check-toggle"
+ type="checkbox"
+ oncommand="toggleSpellCheckingEnabled();"/>
+ <menuitem id="spellCheckAddDictionariesMain"
+ label="&spellAddDictionaries.label;"
+ accesskey="&spellAddDictionaries.accesskey;"
+ oncommand="openDictionaryList();"/>
+ <menu id="spellCheckDictionaries"
+ data-l10n-id="text-action-spell-dictionaries">
+ <menupopup id="spellCheckDictionariesMenu">
+ <menuseparator id="spellCheckLanguageSeparator"/>
+ <menuitem id="spellCheckAddDictionaries"
+ label="&spellAddDictionaries.label;"
+ accesskey="&spellAddDictionaries.accesskey;"
+ oncommand="openDictionaryList();"/>
+ </menupopup>
+ </menu>
+
+</menupopup>
+
+<menupopup id="msgComposeAttachmentItemContext"
+ onpopupshowing="updateAttachmentItems();">
+ <menuitem id="composeAttachmentContext_openItem"
+ label="&openAttachment.label;"
+ accesskey="&openAttachment.accesskey;"
+ command="cmd_openAttachment"/>
+ <menuitem id="composeAttachmentContext_renameItem"
+ label="&renameAttachment.label;"
+ accesskey="&renameAttachment.accesskey;"
+ command="cmd_renameAttachment"/>
+ <menuitem id="composeAttachmentContext_reorderItem"
+ label="&reorderAttachments.label;"
+ accesskey="&reorderAttachments.accesskey;"
+ command="cmd_reorderAttachments"/>
+ <menuseparator id="composeAttachmentContext_beforeRemoveSeparator"/>
+ <menuitem id="composeAttachmentContext_deleteItem"
+ label="&removeAttachment.label;"
+ accesskey="&removeAttachment.accesskey;"
+ command="cmd_delete"/>
+ <menu id="composeAttachmentContext_convertCloudMenu"
+ label="&convertCloud.label;"
+ accesskey="&convertCloud.accesskey;"
+ command="cmd_convertCloud">
+ <menupopup id="convertCloudMenuItems_popup"
+ onpopupshowing="addConvertCloudMenuItems(this, 'convertCloudSeparator', 'context_convertCloud');">
+ <menuitem id="convertCloudMenuItems_popup_convertAttachment"
+ type="radio" name="context_convertCloud"
+ label="&convertRegularAttachment.label;"
+ accesskey="&convertRegularAttachment.accesskey;"
+ command="cmd_convertAttachment"/>
+ <menuseparator id="convertCloudSeparator"/>
+ </menupopup>
+ </menu>
+ <menuitem id="composeAttachmentContext_cancelUploadItem"
+ label="&cancelUpload.label;"
+ accesskey="&cancelUpload.accesskey;"
+ command="cmd_cancelUpload"/>
+ <menuseparator/>
+ <menuitem id="composeAttachmentContext_selectAllItem"
+ label="&selectAll.label;"
+ accesskey="&selectAll.accesskey;"
+ command="cmd_selectAll"/>
+</menupopup>
+
+<menupopup id="msgComposeAttachmentListContext"
+ onpopupshowing="updateAttachmentItems();">
+ <menuitem id="attachmentListContext_selectAllItem"
+ label="&selectAll.label;"
+ accesskey="&selectAll.accesskey;"
+ command="cmd_selectAll"/>
+ <menuseparator/>
+ <menuitem id="attachmentListContext_attachFileItem"
+ data-l10n-id="context-menuitem-attach-files"
+ data-l10n-attrs="acceltext"
+ command="cmd_attachFile"/>
+ <menu id="attachmentListContext_attachCloudMenu"
+ label="&attachCloud.label;"
+ accesskey="&attachCloud.accesskey;"
+ command="cmd_attachCloud">
+ <menupopup id="attachCloudMenu_attachCloudPopup" onpopupshowing="if (event.target == this) { addAttachCloudMenuItems(this); }"/>
+ </menu>
+ <menuitem id="attachmentListContext_attachPageItem"
+ label="&attachPage.label;"
+ accesskey="&attachPage.accesskey;"
+ command="cmd_attachPage"/>
+ <menuseparator id="attachmentListContext_remindLaterSeparator"/>
+ <menuitem id="attachmentListContext_remindLaterItem"
+ type="checkbox"
+ label="&remindLater.label;"
+ accesskey="&remindLater.accesskey;"
+ command="cmd_remindLater"/>
+ <menuitem id="attachmentListContext_reorderItem"
+ label="&reorderAttachments.label;"
+ accesskey="&reorderAttachments.accesskey;"
+ command="cmd_reorderAttachments"/>
+ <menuseparator id="attachmentListContext_removeAllSeparator"/>
+ <menuitem id="attachmentListContext_removeAllItem"
+ label="&removeAllAttachments.label;"
+ accesskey="&removeAllAttachments.accesskey;"
+ command="cmd_removeAllAttachments"/>
+</menupopup>
+
+<menupopup id="attachmentHeaderContext"
+ onpopupshowing="attachmentHeaderContextOnPopupShowing();">
+ <menuitem id="attachmentHeaderContext_initiallyShowItem"
+ type="checkbox"
+ label="&initiallyShowAttachmentPane.label;"
+ accesskey="&initiallyShowAttachmentPane.accesskey;"
+ oncommand="toggleInitiallyShowAttachmentPane(this);"/>
+</menupopup>
+
+<menupopup id="format-toolbar-context-menu"
+ onpopupshowing="ToolbarContextMenu.updateExtension(this);">
+ <menuitem oncommand="ToolbarContextMenu.openAboutAddonsForContextAction(this.parentElement)"
+ data-l10n-id="toolbar-context-menu-manage-extension"
+ class="customize-context-manageExtension"/>
+ <menuitem oncommand="ToolbarContextMenu.removeExtensionForContextAction(this.parentElement)"
+ data-l10n-id="toolbar-context-menu-remove-extension"
+ class="customize-context-removeExtension"/>
+</menupopup>
+
+<menupopup id="toolbar-context-menu"
+ onpopupshowing="onViewToolbarsPopupShowing(event, 'compose-toolbox'); ToolbarContextMenu.updateExtension(this);">
+ <menuseparator/>
+ <menuitem id="CustomizeComposeToolbar"
+ command="cmd_CustomizeComposeToolbar"
+ label="&customizeToolbar.label;"
+ accesskey="&customizeToolbar.accesskey;"/>
+ <menuseparator id="extensionsMailToolbarMenuSeparator"/>
+ <menuitem oncommand="ToolbarContextMenu.openAboutAddonsForContextAction(this.parentElement)"
+ data-l10n-id="toolbar-context-menu-manage-extension"
+ class="customize-context-manageExtension"/>
+ <menuitem oncommand="ToolbarContextMenu.removeExtensionForContextAction(this.parentElement)"
+ data-l10n-id="toolbar-context-menu-remove-extension"
+ class="customize-context-removeExtension"/>
+</menupopup>
+
+<menupopup id="blockedContentOptions" value=""
+ onpopupshowing="onBlockedContentOptionsShowing(event);">
+</menupopup>
+
+<menupopup id="languageMenuList"
+ oncommand="ChangeLanguage(event);"
+ onpopupshowing="OnShowDictionaryMenu(event.target);"
+ onpopupshown="languageMenuListOpened();"
+ onpopuphidden="languageMenuListClosed();">
+</menupopup>
+
+<menupopup id="emailAddressPillPopup"
+ class="emailAddressPopup"
+ onpopupshowing="onPillPopupShowing(event);">
+ <menuitem id="editAddressPill"
+ class="pill-action-edit"
+ data-l10n-id="pill-action-edit"
+ oncommand="editAddressPill(this.parentNode.triggerNode, event)"/>
+ <menuitem id="menu_delete"
+ data-l10n-id="text-action-delete"
+ oncommand="deleteSelectedPillsOnCommand()"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"
+ data-l10n-id="text-action-cut"
+ oncommand="cutSelectedPillsOnCommand()"/>
+ <menuitem id="menu_copy"
+ data-l10n-id="text-action-copy"
+ oncommand="copySelectedPillsOnCommand()"/>
+ <menuseparator id="pillContextBeforeSelectAllSeparator"/>
+ <menuitem id="menu_selectAllSiblingPills"
+ oncommand="selectAllSiblingPillsOnCommand(this.parentNode.triggerNode)"/>
+ <menuitem id="menu_selectAllPills"
+ data-l10n-id="pill-action-select-all-pills"
+ oncommand="selectAllPillsOnCommand()"/>
+ <menuseparator id="pillContextBeforeMoveItemsSeparator"/>
+ <menuitem id="moveAddressPillTo"
+ class="pill-action-move"
+ data-l10n-id="pill-action-move-to"
+ oncommand="moveSelectedPillsOnCommand('addressRowTo')"/>
+ <menuitem id="moveAddressPillCc"
+ class="pill-action-move"
+ data-l10n-id="pill-action-move-cc"
+ oncommand="moveSelectedPillsOnCommand('addressRowCc')"/>
+ <menuitem id="moveAddressPillBcc"
+ class="pill-action-move"
+ data-l10n-id="pill-action-move-bcc"
+ oncommand="moveSelectedPillsOnCommand('addressRowBcc')"/>
+ <menuseparator id="pillContextBeforeExpandListSeparator"/>
+ <menuitem id="expandList"
+ class="pill-action-edit"
+ data-l10n-id="pill-action-expand-list"
+ hidden="true"
+ oncommand="expandList(this.parentNode.triggerNode)"/>
+</menupopup>
+
+ <toolbox id="compose-toolbox"
+ class="toolbox-top"
+ mode="full"
+ defaultmode="full"
+#ifdef XP_MACOSX
+ iconsize="small"
+ defaulticonsize="small"
+#endif
+ labelalign="end"
+ defaultlabelalign="end">
+
+#ifdef XP_MACOSX
+ <hbox id="titlebar">
+ <hbox id="titlebar-title" align="center" flex="1">
+ <label id="titlebar-title-label" value="&msgComposeWindow.title;" flex="1" crop="end"/>
+ </hbox>
+#include ../../../base/content/messenger-titlebar-items.inc.xhtml
+ </hbox>
+#endif
+ <!-- Menu -->
+ <!-- if you change the id of the menubar, be sure to update mailCore.js::CustomizeMailToolbar and MailToolboxCustomizeDone -->
+ <toolbar is="customizable-toolbar" id="compose-toolbar-menubar2"
+ class="chromeclass-menubar themeable-full"
+ type="menubar"
+ customizable="true"
+#ifdef XP_MACOSX
+ defaultset="menubar-items"
+#else
+ defaultset="menubar-items,spring"
+#endif
+#ifdef XP_WIN
+ toolbarname="&menubarCmd.label;"
+ accesskey="&menubarCmd.accesskey;"
+#endif
+ context="toolbar-context-menu" mode="full">
+
+ <toolbaritem id="menubar-items" align="center">
+ <menubar id="mail-menubar">
+ <menu id="menu_File" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+ <menupopup id="menu_FilePopup">
+ <menu id="menu_New"
+ label="&newMenu.label;" accesskey="&newMenu.accesskey;">
+ <menupopup id="menu_NewPopup">
+ <menuitem id="menu_NewMessage"
+ label="&newMessage.label;" accesskey="&newMessage.accesskey;"
+ key="key_newMessage" oncommand="goOpenNewMessage(event);"/>
+ <menuseparator/>
+ <menuitem id="menu_NewContact" label="&newContact.label;"
+ accesskey="&newContact.accesskey;"
+ oncommand="toAddressBook({ action: 'create' });"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_Attach" label="&attachMenu.label;" accesskey="&attachMenu.accesskey;">
+ <menupopup id="menu_AttachPopup" onpopupshowing="updateAttachmentItems();">
+ <menuitem data-l10n-id="menuitem-attach-files"
+ data-l10n-attrs="acceltext"
+ command="cmd_attachFile"/>
+ <menu label="&attachCloudCmd.label;" accesskey="&attachCloudCmd.accesskey;"
+ command="cmd_attachCloud">
+ <menupopup onpopupshowing="if (event.target == this) { addAttachCloudMenuItems(this); }"/>
+ </menu>
+ <menuitem label="&attachPageCmd.label;"
+ accesskey="&attachPageCmd.accesskey;" command="cmd_attachPage"/>
+ <menuseparator/>
+ <menuitem type="checkbox"
+ data-l10n-id="context-menuitem-attach-vcard"
+ command="cmd_attachVCard"/>
+ <menuitem id="menu_AttachPopup_attachPublicKey" type="checkbox"
+ data-l10n-id="context-menuitem-attach-openpgp-key"
+ command="cmd_attachPublicKey"/>
+ <menuseparator id="menu_Attach_RemindLaterSeparator"/>
+ <menuitem id="menu_Attach_RemindLaterItem" type="checkbox" label="&remindLater.label;"
+ accesskey="&remindLater.accesskey;" command="cmd_remindLater"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menuitem id="menu_SaveCmd" label="&saveCmd.label;" accesskey="&saveCmd.accesskey;"
+ key="key_save" command="cmd_saveDefault"/>
+ <menu id="menu_SaveAsCmd"
+ label="&saveAsCmd.label;" accesskey="&saveAsCmd.accesskey;">
+ <menupopup id="menu_SaveAsCmdPopup" onpopupshowing="InitFileSaveAsMenu();">
+ <menuitem id="menu_SaveAsFileCmd"
+ label="&saveAsFileCmd.label;" accesskey="&saveAsFileCmd.accesskey;"
+ command="cmd_saveAsFile" type="radio" name="radiogroup_SaveAs"/>
+ <menuseparator/>
+ <menuitem label="&saveAsDraftCmd.label;" accesskey="&saveAsDraftCmd.accesskey;"
+ command="cmd_saveAsDraft" type="radio" name="radiogroup_SaveAs"/>
+ <menuitem label="&saveAsTemplateCmd.label;" accesskey="&saveAsTemplateCmd.accesskey;"
+ command="cmd_saveAsTemplate" type="radio" name="radiogroup_SaveAs"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menuitem label="&sendNowCmd.label;"
+ accesskey="&sendNowCmd.accesskey;" key="key_send"
+ command="cmd_sendNow" id="menu-item-send-now"/>
+ <menuitem label="&sendLaterCmd.label;"
+ accesskey="&sendLaterCmd.accesskey;" key="key_sendLater"
+ command="cmd_sendLater"/>
+ <menuseparator/>
+ <menuitem id="printMenuItem"
+ label="&printCmd.label;" accesskey="&printCmd.accesskey;"
+ key="key_print" command="cmd_print"/>
+ <menuseparator id="menu_FileCloseSeparator"/>
+ <menuitem id="menu_close"
+ label="&closeCmd.label;"
+ key="key_close"
+ accesskey="&closeCmd.accesskey;"
+ command="cmd_close"/>
+ </menupopup>
+ </menu>
+
+ <!-- Edit Menu -->
+ <menu id="menu_Edit" label="&editMenu.label;" accesskey="&editMenu.accesskey;">
+ <menupopup id="menu_EditPopup" onpopupshowing="updateEditItems();">
+ <menuitem id="menu_undo"
+ data-l10n-id="text-action-undo"
+ key="key_undo" command="cmd_undo"/>
+ <menuitem id="menu_redo"
+ data-l10n-id="text-action-redo"
+ key="key_redo" command="cmd_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"
+ data-l10n-id="text-action-cut"
+ key="key_cut" command="cmd_cut"/>
+ <menuitem id="menu_copy"
+ data-l10n-id="text-action-copy"
+ key="key_copy" command="cmd_copy"/>
+ <menuitem id="menu_paste"
+ data-l10n-id="text-action-paste"
+ key="key_paste" command="cmd_paste"/>
+ <menuitem id="menu_pasteNoFormatting"
+ command="cmd_pasteNoFormatting" key="pastenoformattingkb"/>
+ <menuitem id="menu_pasteQuote"
+ accesskey="&pasteAsQuotationCmd.accesskey;"
+ command="cmd_pasteQuote"
+ key="pastequotationkb"/>
+ <menuitem id="menu_delete"
+ data-l10n-id="text-action-delete"
+ key="key_delete"
+ command="cmd_delete"/>
+ <menuseparator/>
+ <menuitem id="menu_rewrap"
+ label="&editRewrapCmd.label;"
+ accesskey="&editRewrapCmd.accesskey;"
+ key="key_rewrap"
+ command="cmd_rewrap"/>
+ <menuitem id="menu_RenameAttachment"
+ label="&renameAttachmentCmd.label;"
+ accesskey="&renameAttachmentCmd.accesskey;"
+ key="key_renameAttachment"
+ command="cmd_renameAttachment"/>
+ <menuitem id="menu_reorderAttachments"
+ label="&reorderAttachmentsCmd.label;"
+ accesskey="&reorderAttachmentsCmd.accesskey;"
+ key="key_reorderAttachments"
+ command="cmd_reorderAttachments"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"
+ data-l10n-id="text-action-select-all"
+ key="key_selectAll"
+ command="cmd_selectAll"/>
+ <menuseparator/>
+ <menuitem id="menu_findBar"
+ label="&findBarCmd.label;"
+ accesskey="&findBarCmd.accesskey;"
+ key="key_find"
+ command="cmd_find"/>
+#ifndef XP_MACOSX
+ <menuitem id="menu_findReplace"
+ label="&findReplaceCmd.label;"
+ accesskey="&findReplaceCmd.accesskey;"
+ key="key_findReplace"
+ command="cmd_findReplace"/>
+#else
+ <menuitem id="menu_findReplace"
+ label="&findReplaceCmd.label;"
+ accesskey="&findReplaceCmd.accesskey;"
+ command="cmd_findReplace"/>
+#endif
+ <menuitem id="menu_findNext"
+ label="&findAgainCmd.label;"
+ accesskey="&findAgainCmd.accesskey;"
+ key="key_findNext"
+ command="cmd_findNext"/>
+ <menuitem id="menu_findPrev"
+ label="&findPrevCmd.label;"
+ accesskey="&findPrevCmd.accesskey;"
+ key="key_findPrev"
+ command="cmd_findPrev"/>
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+ <menuseparator id="prefSep"/>
+ <menuitem id="menu_accountmgr"
+ label="&accountManagerCmd2.label;"
+ accesskey="&accountManagerCmdUnix2.accesskey;"
+ command="cmd_account"/>
+ <menuitem id="menu_preferences"
+ data-l10n-id="menu-tools-settings"
+ oncommand="openOptionsDialog('paneCompose');"/>
+#endif
+#endif
+ </menupopup>
+ </menu>
+
+ <!-- View Menu -->
+ <menu id="menu_View" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
+ <menupopup id="menu_View_Popup" onpopupshowing="updateViewItems();">
+ <menu id="menu_ToolbarsNew"
+ label="&viewToolbarsMenuNew.label;"
+ accesskey="&viewToolbarsMenuNew.accesskey;"
+ onpopupshowing="onViewToolbarsPopupShowing(event, 'compose-toolbox');">
+ <menupopup id="view_toolbars_popup">
+ <menuitem id="menu_showFormatToolbar"
+ type="checkbox"
+ label="&showFormattingBarCmd.label;"
+ accesskey="&showFormattingBarCmd.accesskey;"
+ command="cmd_showFormatToolbar"
+ checked="true"/>
+ <menuitem id="menu_showTaskbar"
+ type="checkbox"
+ label="&showTaskbarCmd.label;"
+ accesskey="&showTaskbarCmd.accesskey;"
+ oncommand="goToggleToolbar('status-bar', 'menu_showTaskbar')"
+ checked="true"/>
+ <menuseparator id="viewMenuBeforeCustomizeComposeToolbarsSeparator"/>
+ <menuitem id="customizeComposeToolbars"
+ label="&customizeToolbar.label;"
+ accesskey="&customizeToolbar.accesskey;"
+ command="cmd_CustomizeComposeToolbar"/>
+ </menupopup>
+ </menu>
+ <menu id="viewFullZoomMenu" label="&fullZoom.label;" accesskey="&fullZoom.accesskey;"
+ onpopupshowing="UpdateFullZoomMenu()">
+ <menupopup id="viewFullZoomPopupMenu">
+ <menuitem id="menu_fullZoomEnlarge" key="key_fullZoomEnlarge"
+ label="&fullZoomEnlargeCmd.label;"
+ accesskey="&fullZoomEnlargeCmd.accesskey;"
+ command="cmd_fullZoomEnlarge"/>
+ <menuitem id="menu_fullZoomReduce" key="key_fullZoomReduce"
+ label="&fullZoomReduceCmd.label;"
+ accesskey="&fullZoomReduceCmd.accesskey;"
+ command="cmd_fullZoomReduce"/>
+ <menuseparator id="fullZoomAfterReduceSeparator"/>
+ <menuitem id="menu_fullZoomReset" key="key_fullZoomReset"
+ label="&fullZoomResetCmd.label;"
+ accesskey="&fullZoomResetCmd.accesskey;"
+ command="cmd_fullZoomReset"/>
+ <menuseparator id="fullZoomAfterResetSeparator"/>
+ <menuitem id="menu_fullZoomToggle" label="&fullZoomToggleCmd.label;"
+ accesskey="&fullZoomToggleCmd.accesskey;"
+ type="checkbox" command="cmd_fullZoomToggle" checked="false"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="viewMenuBeforeShowToFieldSeparator"/>
+ <menuitem id="menu_showToField"
+ data-l10n-attrs="acceltext"
+ oncommand="showAndFocusAddressRow('addressRowTo')"/>
+ <menuitem id="menu_showCcField"
+ data-l10n-attrs="acceltext"
+ oncommand="showAndFocusAddressRow('addressRowCc')"/>
+ <menuitem id="menu_showBccField"
+ data-l10n-attrs="acceltext"
+ oncommand="showAndFocusAddressRow('addressRowBcc')"/>
+ <menuseparator id="viewMenuBeforeAddressSidebarSeparator"/>
+ <menuitem id="menu_AddressSidebar"
+ label="&addressSidebar.label;" accesskey="&addressSidebar.accesskey;"
+ type="checkbox"
+ key="key_addressSidebar"
+ oncommand="toggleContactsSidebar();"/>
+ <menuitem id="menu_toggleAttachmentPane"
+ data-l10n-id="menuitem-toggle-attachment-pane"
+ data-l10n-attrs="acceltext"
+ type="checkbox"
+ command="cmd_toggleAttachmentPane"/>
+ </menupopup>
+ </menu>
+
+ <menu id="insertMenu" label="&insertMenu.label;"
+ accesskey="&insertMenu.accesskey;" command="cmd_renderedHTMLEnabler">
+ <menupopup id="insertMenuPopup">
+ <menuitem id="insertImage"
+ label="&insertImageCmd.label;"
+ accesskey="&insertImageCmd.accesskey;"
+ observes="cmd_image"/>
+ <menuitem id="insertTable"
+ label="&insertTableCmd.label;"
+ accesskey="&insertTableCmd.accesskey;"
+ observes="cmd_InsertTable"/>
+ <menuitem id="insertLink"
+ label="&insertLinkCmd2.label;"
+ accesskey="&insertLinkCmd2.accesskey;"
+ key="insertlinkkb"
+ observes="cmd_link"/>
+ <menuitem id="insertAnchor"
+ label="&insertAnchorCmd.label;"
+ accesskey="&insertAnchorCmd.accesskey;"
+ observes="cmd_anchor"/>
+ <menuitem id="insertHline"
+ label="&insertHLineCmd.label;"
+ accesskey="&insertHLineCmd.accesskey;"
+ observes="cmd_hline"/>
+ <menuitem id="insertHTMLSource"
+ accesskey="&insertHTMLCmd.accesskey;"
+ observes="cmd_insertHTMLWithDialog"/>
+ <menuitem id="insertMath"
+ accesskey="&insertMathCmd.accesskey;"
+ observes="cmd_insertMathWithDialog"/>
+ <menuitem id="insertChars"
+ accesskey="&insertCharsCmd.accesskey;"
+ command="cmd_insertChars"/>
+
+ <menu id="insertTOC" label="&tocMenu.label;" accesskey="&tocMenu.accesskey;">
+ <menupopup id="insertTOCPopup" onpopupshowing="InitTOCMenu()">
+ <menuitem id="insertTOCMenuitem"
+ label="&insertTOC.label;"
+ accesskey="&insertTOC.accesskey;"
+ oncommand="UpdateTOC()"/>
+ <menuitem id="updateTOCMenuitem"
+ label="&updateTOC.label;"
+ accesskey="&updateTOC.accesskey;"
+ oncommand="UpdateTOC()"/>
+ <menuitem id="removeTOCMenuitem"
+ label="&removeTOC.label;"
+ accesskey="&removeTOC.accesskey;"
+ oncommand="RemoveTOC()"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="insertMenuSeparator"/>
+ <menuitem id="insertBreakAll"
+ accesskey="&insertBreakAllCmd.accesskey;"
+ observes="cmd_insertBreakAll"
+ label="&insertBreakAllCmd.label;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="formatMenu" label="&formatMenu.label;" accesskey="&formatMenu.accesskey;" command="cmd_renderedHTMLEnabler">
+ <menupopup id="formatMenuPopup" onpopupshowing="EditorInitFormatMenu()">
+ <!-- Font face submenu -->
+ <menu id="fontFaceMenu"
+ label="&fontfaceMenu.label;" accesskey="&fontfaceMenu.accesskey;"
+ position="1">
+ <menupopup id="fontFaceMenuPopup"
+ oncommand="if (event.target.localName == 'menuitem') {
+ doStatefulCommand('cmd_fontFace', event.target.getAttribute('value'));
+ }"
+ onpopupshowing="initFontFaceMenu(this);">
+ <menuitem id="menu_fontFaceVarWidth"
+ label="&fontVarWidth.label;"
+ accesskey="&fontVarWidth.accesskey;"
+ value=""
+ type="radio"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_fontFaceFixedWidth"
+ label="&fontFixedWidth.label;"
+ accesskey="&fontFixedWidth.accesskey;"
+ value="monospace"
+ type="radio"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuseparator id="fontFaceMenuAfterGenericFontsSeparator"/>
+ <menuitem id="menu_fontFaceHelvetica"
+ label="&fontHelvetica.label;"
+ accesskey="&fontHelvetica.accesskey;"
+ value="Helvetica, Arial, sans-serif"
+ value_parsed="helvetica,arial,sans-serif"
+ type="radio"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_fontFaceTimes"
+ label="&fontTimes.label;"
+ accesskey="&fontTimes.accesskey;"
+ value="Times New Roman, Times, serif"
+ value_parsed="times new roman,times,serif"
+ type="radio"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_fontFaceCourier"
+ label="&fontCourier.label;"
+ accesskey="&fontCourier.accesskey;"
+ value="Courier New, Courier, monospace"
+ value_parsed="courier new,courier,monospace"
+ type="radio"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuseparator id="fontFaceMenuAfterDefaultFontsSeparator"
+ class="fontFaceMenuAfterDefaultFonts"/>
+ <menuseparator id="fontFaceMenuAfterUsedFontsSeparator"
+ class="fontFaceMenuAfterUsedFonts"
+ hidden="true"/>
+ <!-- Local font face items added here by initLocalFontFaceMenu() -->
+ </menupopup>
+ </menu>
+
+ <!-- Font size submenu -->
+ <menu id="fontSizeMenu" label="&fontSizeMenu.label;"
+ accesskey="&fontSizeMenu.accesskey;"
+ position="2">
+ <menupopup id="fontSizeMenuPopup"
+ onpopupshowing="initFontSizeMenu(this)"
+ oncommand="setFontSize(event)">
+ <menuitem id="menu_decreaseFontSize"
+ label="&decreaseFontSize.label;"
+ accesskey="&decreaseFontSize.accesskey;"
+ key="decreasefontsizekb"
+ observes="cmd_decreaseFontStep"
+ type="radio" name="decreaseFontSize" autocheck="false"/>
+ <menuitem id="menu_increaseFontSize"
+ label="&increaseFontSize.label;"
+ accesskey="&increaseFontSize.accesskey;"
+ key="increasefontsizekb"
+ observes="cmd_increaseFontStep"
+ type="radio" name="increaseFontSize" autocheck="false"/>
+ <menuseparator id="fontSizeMenuAfterIncreaseFontSizeSeparator"/>
+ <menuitem id="menu_size-x-small"
+ label="&size-tinyCmd.label;"
+ accesskey="&size-tinyCmd.accesskey;"
+ value="1"
+ type="radio" name="fontSize"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_size-small"
+ label="&size-smallCmd.label;"
+ accesskey="&size-smallCmd.accesskey;"
+ value="2"
+ type="radio" name="fontSize"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_size-medium"
+ label="&size-mediumCmd.label;"
+ accesskey="&size-mediumCmd.accesskey;"
+ value="3"
+ type="radio" name="fontSize"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_size-large"
+ label="&size-largeCmd.label;"
+ accesskey="&size-largeCmd.accesskey;"
+ value="4"
+ type="radio" name="fontSize"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_size-x-large"
+ label="&size-extraLargeCmd.label;"
+ accesskey="&size-extraLargeCmd.accesskey;"
+ value="5"
+ type="radio" name="fontSize"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_size-xx-large"
+ label="&size-hugeCmd.label;"
+ accesskey="&size-hugeCmd.accesskey;"
+ value="6"
+ type="radio" name="fontSize"
+ observes="cmd_renderedHTMLEnabler"/>
+ </menupopup>
+ </menu>
+
+ <!-- Font style submenu -->
+ <menu id="fontStyleMenu" label="&fontStyleMenu.label;"
+ accesskey="&fontStyleMenu.accesskey;"
+ position="3">
+ <menupopup id="fontStyleMenuPopup" onpopupshowing="initFontStyleMenu(this)">
+ <menuitem id="menu_styleBold"
+ label="&styleBoldCmd.label;"
+ accesskey="&styleBoldCmd.accesskey;"
+ observes="cmd_bold"
+ type="checkbox"
+ key="boldkb"/>
+ <menuitem id="menu_styleItalic"
+ label="&styleItalicCmd.label;"
+ accesskey="&styleItalicCmd.accesskey;"
+ observes="cmd_italic"
+ type="checkbox"
+ key="italickb"/>
+ <menuitem id="menu_styleUnderline"
+ label="&styleUnderlineCmd.label;"
+ accesskey="&styleUnderlineCmd.accesskey;"
+ observes="cmd_underline"
+ type="checkbox"
+ key="underlinekb"/>
+ <menuitem id="menu_styleStrikeThru"
+ label="&styleStrikeThruCmd.label;"
+ accesskey="&styleStrikeThruCmd.accesskey;"
+ observes="cmd_strikethrough"
+ type="checkbox"/>
+ <menuitem id="menu_styleSuperscript"
+ label="&styleSuperscriptCmd.label;"
+ accesskey="&styleSuperscriptCmd.accesskey;"
+ observes="cmd_superscript"
+ type="checkbox"/>
+ <menuitem id="menu_styleSubscript"
+ label="&styleSubscriptCmd.label;"
+ accesskey="&styleSubscriptCmd.accesskey;"
+ observes="cmd_subscript"
+ type="checkbox"/>
+ <menuitem id="menu_fontFixedWidth"
+ label="&fontFixedWidth.label;"
+ accesskey="&fontFixedWidth.accesskey;"
+ observes="cmd_tt"
+ type="checkbox"
+ key="fixedwidthkb"/>
+ <menuitem id="menu_styleNonbreaking"
+ label="&styleNonbreakingCmd.label;"
+ accesskey="&styleNonbreakingCmd.accesskey;"
+ observes="cmd_nobreak"
+ type="checkbox"/>
+ <menuseparator id="fontStyleMenuAfterNonbreakingSeparator"/>
+ <menuitem id="menu_styleEm"
+ label="&styleEm.label;"
+ accesskey="&styleEm.accesskey;"
+ observes="cmd_em"
+ type="checkbox"/>
+ <menuitem id="menu_styleStrong"
+ label="&styleStrong.label;"
+ accesskey="&styleStrong.accesskey;"
+ observes="cmd_strong"
+ type="checkbox"/>
+ <menuitem id="menu_styleCite"
+ label="&styleCite.label;"
+ accesskey="&styleCite.accesskey;"
+ observes="cmd_cite"
+ type="checkbox"/>
+ <menuitem id="menu_styleAbbr"
+ label="&styleAbbr.label;"
+ accesskey="&styleAbbr.accesskey;"
+ observes="cmd_abbr"
+ type="checkbox"/>
+ <menuitem id="menu_styleAcronym"
+ label="&styleAcronym.label;"
+ accesskey="&styleAcronym.accesskey;"
+ observes="cmd_acronym"
+ type="checkbox"/>
+ <menuitem id="menu_styleCode"
+ label="&styleCode.label;"
+ accesskey="&styleCode.accesskey;"
+ observes="cmd_code"
+ type="checkbox"/>
+ <menuitem id="menu_styleSamp"
+ label="&styleSamp.label;"
+ accesskey="&styleSamp.accesskey;"
+ observes="cmd_samp"
+ type="checkbox"/>
+ <menuitem id="menu_styleVar"
+ label="&styleVar.label;"
+ accesskey="&styleVar.accesskey;"
+ observes="cmd_var"
+ type="checkbox"/>
+ </menupopup>
+ </menu>
+
+ <!-- Note: "cmd_fontColor" only monitors color state, it doesn't execute the command
+ (We should use "cmd_fontColorState" and "cmd_backgroundColorState" ?) -->
+ <menuitem id="fontColor" label="&formatFontColor.label;"
+ accesskey="&formatFontColor.accesskey;"
+ observes="cmd_fontColor"
+ oncommand="EditorSelectColor('Text', null);"
+ position="4"/>
+ <menuseparator id="removeSep" position="5"/>
+
+ <!-- label and accesskey set at runtime from strings -->
+ <menuitem id="removeStylesMenuitem" key="removestyleskb"
+ observes="cmd_removeStyles"
+ position="6"/>
+ <menuitem id="removeLinksMenuitem" key="removelinkskb"
+ observes="cmd_removeLinks"
+ position="7"/>
+ <menuitem id="removeNamedAnchorsMenuitem" label="&formatRemoveNamedAnchors.label;"
+ key="removenamedanchorskb"
+ accesskey="&formatRemoveNamedAnchors.accesskey;"
+ observes="cmd_removeNamedAnchors"
+ position="8"/>
+ <menuseparator id="tabSep" position="9"/>
+
+ <!-- Note: the 'Init' menu methods for Paragraph, List, and Align
+ assume that the id = 'menu_'+tagName (the 'value' label),
+ except for the first ('none') item
+ -->
+ <!-- Paragraph Style submenu -->
+ <menu id="paragraphMenu" label="&paragraphMenu.label;"
+ accesskey="&paragraphMenu.accesskey;"
+ position="10" onpopupshowing="InitParagraphMenu()">
+ <menupopup id="paragraphMenuPopup"
+ oncommand="setParagraphState(event);">
+ <menuitem id="menu_bodyText"
+ type="radio"
+ name="1"
+ label="&bodyTextCmd.label;"
+ accesskey="&bodyTextCmd.accesskey;"
+ value=""
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_p"
+ type="radio"
+ name="1"
+ label="&paragraphParagraphCmd.label;"
+ accesskey="&paragraphParagraphCmd.accesskey;"
+ value="p"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_h1"
+ type="radio"
+ name="1"
+ label="&heading1Cmd.label;"
+ accesskey="&heading1Cmd.accesskey;"
+ value="h1"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_h2"
+ type="radio"
+ name="1"
+ label="&heading2Cmd.label;"
+ accesskey="&heading2Cmd.accesskey;"
+ value="h2"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_h3"
+ type="radio"
+ name="1"
+ label="&heading3Cmd.label;"
+ accesskey="&heading3Cmd.accesskey;"
+ value="h3"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_h4"
+ type="radio"
+ name="1"
+ label="&heading4Cmd.label;"
+ accesskey="&heading4Cmd.accesskey;"
+ value="h4"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_h5"
+ type="radio"
+ name="1"
+ label="&heading5Cmd.label;"
+ accesskey="&heading5Cmd.accesskey;"
+ value="h5"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_h6"
+ type="radio"
+ name="1"
+ label="&heading6Cmd.label;"
+ accesskey="&heading6Cmd.accesskey;"
+ value="h6"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_address"
+ type="radio"
+ name="1"
+ label="&paragraphAddressCmd.label;"
+ accesskey="&paragraphAddressCmd.accesskey;"
+ value="address"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_pre"
+ type="radio"
+ name="1"
+ label="&paragraphPreformatCmd.label;"
+ accesskey="&paragraphPreformatCmd.accesskey;"
+ value="pre"
+ observes="cmd_renderedHTMLEnabler"/>
+ </menupopup>
+ </menu>
+
+ <!-- List Style submenu -->
+ <menu id="listMenu" label="&formatlistMenu.label;"
+ accesskey="&formatlistMenu.accesskey;"
+ position="11" onpopupshowing="InitListMenu()">
+ <menupopup id="listMenuPopup">
+ <menuitem id="menu_noList"
+ type="radio"
+ name="1"
+ label="&noneCmd.label;"
+ accesskey="&noneCmd.accesskey;"
+ observes="cmd_removeList"/>
+ <menuitem id="menu_ul"
+ type="radio"
+ name="1"
+ label="&listBulletCmd.label;"
+ accesskey="&listBulletCmd.accesskey;"
+ observes="cmd_ul"/>
+ <menuitem id="menu_ol"
+ type="radio"
+ name="1"
+ label="&listNumberedCmd.label;"
+ accesskey="&listNumberedCmd.accesskey;"
+ observes="cmd_ol"/>
+ <menuitem id="menu_dt"
+ type="radio"
+ name="1"
+ label="&listTermCmd.label;"
+ accesskey="&listTermCmd.accesskey;"
+ observes="cmd_dt"/>
+ <menuitem id="menu_dd"
+ type="radio"
+ name="1"
+ label="&listDefinitionCmd.label;"
+ accesskey="&listDefinitionCmd.accesskey;"
+ observes="cmd_dd"/>
+ <menuseparator/>
+ <menuitem id="listProps"
+ label="&listPropsCmd.label;"
+ accesskey="&listPropsCmd.accesskey;"
+ observes="cmd_listProperties"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="identingSep" position="12"/>
+
+ <menuitem id="increaseIndent"
+ label="&increaseIndent.label;"
+ accesskey="&increaseIndent.accesskey;"
+ key="increaseindentkb"
+ observes="cmd_indent"
+ position="13"/>
+ <menuitem id="decreaseIndent"
+ label="&decreaseIndent.label;"
+ accesskey="&decreaseIndent.accesskey;"
+ key="decreaseindentkb"
+ observes="cmd_outdent"
+ position="14"/>
+
+ <menu id="alignMenu" label="&alignMenu.label;" accesskey="&alignMenu.accesskey;"
+ onpopupshowing="InitAlignMenu()"
+ position="15">
+ <!-- Align submenu -->
+ <menupopup id="alignMenuPopup"
+ oncommand="doStatefulCommand('cmd_align', event.target.getAttribute('value'))">
+ <menuitem id="menu_left"
+ label="&alignLeft.label;"
+ accesskey="&alignLeft.accesskey;"
+ type="radio"
+ name="1"
+ value="left"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_center"
+ label="&alignCenter.label;"
+ accesskey="&alignCenter.accesskey;"
+ type="radio"
+ name="1"
+ value="center"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_right"
+ label="&alignRight.label;"
+ accesskey="&alignRight.accesskey;"
+ type="radio"
+ name="1"
+ value="right"
+ observes="cmd_renderedHTMLEnabler"/>
+ <menuitem id="menu_justify"
+ label="&alignJustify.label;"
+ accesskey="&alignJustify.accesskey;"
+ type="radio"
+ name="1"
+ value="justify"
+ observes="cmd_renderedHTMLEnabler"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="tableSep" position="16"/>
+ <menu id="tableMenu" label="&tableMenu.label;" accesskey="&tableMenu.accesskey;">
+ <menupopup id="tableMenuPopup" onpopupshowing="EditorInitTableMenu()">
+ <menu id="tableInsertMenu" label="&tableInsertMenu.label;" accesskey="&tableInsertMenu.accesskey;">
+ <menupopup id="tableMenuPopup">
+ <menuitem id="menu_insertTable"
+ label="&insertTableCmd.label;"
+ accesskey="&insertTableCmd.accesskey;"
+ observes="cmd_InsertTable"/>
+ <menuseparator id="tableMenuAfterInsertTableSeparator"/>
+ <menuitem id="menu_tableRowAbove"
+ label="&tableRowAbove.label;"
+ accesskey="&tableRowAbove.accesskey;"
+ observes="cmd_InsertRowAbove"/>
+ <menuitem id="menu_tableRowBelow"
+ label="&tableRowBelow.label;"
+ accesskey="&tableRowBelow.accesskey;"
+ observes="cmd_InsertRowBelow"/>
+ <menuseparator id="tableMenuAfterTableRowSeparator"/>
+ <menuitem id="menu_tableColumnBefore"
+ label="&tableColumnBefore.label;"
+ accesskey="&tableColumnBefore.accesskey;"
+ observes="cmd_InsertColumnBefore"/>
+ <menuitem id="menu_tableColumnAfter"
+ label="&tableColumnAfter.label;"
+ accesskey="&tableColumnAfter.accesskey;"
+ observes="cmd_InsertColumnAfter"/>
+ <menuseparator id="tableMenuAfterInsertColumnSeparator"/>
+ <menuitem id="menu_tableCellBefore"
+ label="&tableCellBefore.label;"
+ accesskey="&tableCellBefore.accesskey;"
+ observes="cmd_InsertCellBefore"/>
+ <menuitem id="menu_tableCellAfter"
+ label="&tableCellAfter.label;"
+ accesskey="&tableCellAfter.accesskey;"
+ observes="cmd_InsertCellAfter"/>
+ </menupopup>
+ </menu>
+ <menu id="tableSelectMenu"
+ label="&tableSelectMenu.label;"
+ accesskey="&tableSelectMenu.accesskey;" >
+ <menupopup id="tableSelectPopup">
+ <menuitem id="menu_SelectTable"
+ label="&tableTable.label;"
+ accesskey="&tableTable.accesskey;"
+ observes="cmd_SelectTable"/>
+ <menuitem id="menu_SelectRow"
+ label="&tableRow.label;"
+ accesskey="&tableRow.accesskey;"
+ observes="cmd_SelectRow"/>
+ <menuitem id="menu_SelectColumn"
+ label="&tableColumn.label;"
+ accesskey="&tableColumn.accesskey;"
+ observes="cmd_SelectColumn"/>
+ <menuitem id="menu_SelectCell"
+ label="&tableCell.label;"
+ accesskey="&tableCell.accesskey;"
+ observes="cmd_SelectCell"/>
+ <menuitem id="menu_SelectAllCells"
+ label="&tableAllCells.label;"
+ accesskey="&tableAllCells.accesskey;"
+ observes="cmd_SelectAllCells"/>
+ </menupopup>
+ </menu>
+ <menu id="tableDeleteMenu"
+ label="&tableDeleteMenu.label;"
+ accesskey="&tableDeleteMenu.accesskey;">
+ <menupopup id="tableDeletePopup">
+ <menuitem id="menu_DeleteTable"
+ label="&tableTable.label;"
+ accesskey="&tableTable.accesskey;"
+ observes="cmd_DeleteTable"/>
+ <menuitem id="menu_DeleteRow"
+ label="&tableRows.label;"
+ accesskey="&tableRow.accesskey;"
+ observes="cmd_DeleteRow"/>
+ <menuitem id="menu_DeleteColumn"
+ label="&tableColumns.label;"
+ accesskey="&tableColumn.accesskey;"
+ observes="cmd_DeleteColumn"/>
+ <menuitem id="menu_DeleteCell"
+ label="&tableCells.label;"
+ accesskey="&tableCell.accesskey;"
+ observes="cmd_DeleteCell"/>
+ <menuitem id="menu_DeleteCellContents"
+ label="&tableCellContents.label;"
+ accesskey="&tableCellContents.accesskey;"
+ observes="cmd_DeleteCellContents"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <!-- menu label is set in InitTableMenu -->
+ <menuitem id="menu_JoinTableCells"
+ label="&tableJoinCells.label;"
+ accesskey="&tableJoinCells.accesskey;"
+ observes="cmd_JoinTableCells"/>
+ <menuitem id="menu_SlitTableCell"
+ label="&tableSplitCell.label;"
+ accesskey="&tableSplitCell.accesskey;"
+ observes="cmd_SplitTableCell"/>
+ <menuitem id="menu_ConvertToTable"
+ label="&convertToTable.label;"
+ accesskey="&convertToTable.accesskey;"
+ observes="cmd_ConvertToTable"/>
+ <menuseparator/>
+ <menuitem id="menu_TableOrCellColor"
+ label="&tableOrCellColor.label;"
+ accesskey="&tableOrCellColor.accesskey;"
+ observes="cmd_TableOrCellColor"/>
+ <menuitem id="menu_tableProperties"
+ label="&tableProperties.label;"
+ accesskey="&tableProperties.accesskey;"
+ observes="cmd_editTable"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <!-- label and accesskey filled in during menu creation -->
+ <menuitem id="objectProperties"
+ command="cmd_objectProperties"/>
+ <!-- Don't use 'observes', must call command correctly -->
+ <menuitem id="colorsAndBackground"
+ label="&colorsAndBackground.label;"
+ accesskey="&colorsAndBackground.accesskey;"
+ oncommand="goDoCommand('cmd_colorProperties')"
+ observes="cmd_renderedHTMLEnabler"/>
+ </menupopup>
+ </menu>
+
+ <menu id="optionsMenu" label="&optionsMenu.label;" accesskey="&optionsMenu.accesskey;">
+ <menupopup id="optionsMenuPopup" onpopupshowing="updateOptionsMenu();">
+ <menuitem id="menu_checkspelling"
+ label="&checkSpellingCmd2.label;"
+ accesskey="&checkSpellingCmd2.accesskey;"
+ key="key_checkspelling"
+ command="cmd_spelling"/>
+ <menuitem id="menu_inlineSpellCheck"
+ label="&enableInlineSpellChecker.label;"
+ accesskey="&enableInlineSpellChecker.accesskey;"
+ type="checkbox"
+ oncommand="toggleSpellCheckingEnabled();"/>
+ <menuitem id="menu_quoteMessage"
+ label="&quoteCmd.label;"
+ accesskey="&quoteCmd.accesskey;"
+ command="cmd_quoteMessage"/>
+ <menuseparator/>
+ <menuitem id="returnReceiptMenu" type="checkbox"
+ label="&returnReceiptMenu.label;"
+ accesskey="&returnReceiptMenu.accesskey;"
+ checked="false"
+ command="cmd_toggleReturnReceipt"/>
+ <menuitem id="dsnMenu" type="checkbox" label="&dsnMenu.label;" accesskey="&dsnMenu.accesskey;" oncommand="ToggleDSN(event.target)"/>
+ <menuseparator/>
+ <menu id="outputFormatMenu" data-l10n-id="compose-send-format-menu">
+ <menupopup id="outputFormatMenuPopup">
+ <menuitem type="radio" name="output_format" id="format_auto" data-l10n-id="compose-send-auto-menu-item"/>
+ <menuitem type="radio" name="output_format" id="format_both" data-l10n-id="compose-send-both-menu-item"/>
+ <menuitem type="radio" name="output_format" id="format_html" data-l10n-id="compose-send-html-menu-item"/>
+ <menuitem type="radio" name="output_format" id="format_plain" data-l10n-id="compose-send-plain-menu-item"/>
+ </menupopup>
+ </menu>
+ <menu id="priorityMenu" label="&priorityMenu.label;" accesskey="&priorityMenu.accesskey;" onpopupshowing="updatePriorityMenu();" oncommand="PriorityMenuSelect(event.target);">
+ <menupopup id="priorityMenuPopup">
+ <menuitem type="radio" name="priority" label="&highestPriorityCmd.label;" accesskey="&highestPriorityCmd.accesskey;" value="Highest" id="priority_highest"/>
+ <menuitem type="radio" name="priority" label="&highPriorityCmd.label;" accesskey="&highPriorityCmd.accesskey;" value="High" id="priority_high"/>
+ <menuitem type="radio" name="priority" label="&normalPriorityCmd.label;" accesskey="&normalPriorityCmd.accesskey;" value="" id="priority_normal" checked="true"/>
+ <menuitem type="radio" name="priority" label="&lowPriorityCmd.label;" accesskey="&lowPriorityCmd.accesskey;" value="Low" id="priority_low"/>
+ <menuitem type="radio" name="priority" label="&lowestPriorityCmd.label;" accesskey="&lowestPriorityCmd.accesskey;" value="Lowest" id="priority_lowest"/>
+ </menupopup>
+ </menu>
+ <menu id="fccMenu" label="&fileCarbonCopyCmd.label;"
+ accesskey="&fileCarbonCopyCmd.accesskey;"
+ oncommand="MessageFcc(event.target._folder)">
+ <menupopup is="folder-menupopup" id="fccMenuPopup" mode="filing"
+ showFileHereLabel="true" fileHereLabel="&fileHereMenu.label;"/>
+ </menu>
+ <menuseparator/>
+ <menuitem type="checkbox" command="cmd_customizeFromAddress"
+ accesskey="&customizeFromAddress.accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="encryptionMenu" data-l10n-id="encryption-menu">
+ <menupopup onpopupshowing="setSecuritySettings('_Menubar');">
+
+ <menuitem id="encTech_OpenPGP_Menubar"
+ label="&menu_techPGP.label;" accesskey="&menu_techPGP.accesskey;"
+ value="OpenPGP" type="radio" name="radiogroup_encTech"
+ oncommand="onEncryptionChoice(event.target.value);"/>
+ <menuitem id="encTech_SMIME_Menubar"
+ label="&menu_techSMIME.label;" accesskey="&menu_techSMIME.accesskey;"
+ value="SMIME" type="radio" name="radiogroup_encTech"
+ oncommand="onEncryptionChoice(event.target.value);"/>
+
+ <menuseparator id="encryptionOptionsSeparator_Menubar"/>
+
+ <menuitem id="menu_securityEncrypt_Menubar"
+ type="checkbox"
+ data-l10n-id="menu-encrypt"
+ value="enc"
+ oncommand="onEncryptionChoice(event.target.value);"/>
+ <menuitem id="menu_securityEncryptSubject_Menubar"
+ type="checkbox"
+ data-l10n-id="menu-encrypt-subject"
+ value="encsub"
+ oncommand="onEncryptionChoice(event.target.value);"/>
+ <menuitem id="menu_securitySign_Menubar"
+ type="checkbox"
+ data-l10n-id="menu-sign"
+ value="sig"
+ oncommand="onEncryptionChoice(event.target.value);"/>
+
+ <menuseparator id="statusInfoSeparator"/>
+
+ <menuitem id="menu_recipientStatus_Menubar"
+ data-l10n-id="menu-manage-keys"
+ value="status"
+ oncommand="onEncryptionChoice(event.target.value);"/>
+ <menuitem id="menu_openManager_Menubar"
+ data-l10n-id="menu-open-key-manager"
+ value="manager"
+ oncommand="onEncryptionChoice(event.target.value);"/>
+
+ </menupopup>
+ </menu>
+
+ <menu id="tasksMenu" label="&tasksMenu.label;" accesskey="&tasksMenu.accesskey;">
+ <menupopup id="taskPopup">
+ <menuitem id="tasksMenuMail" accesskey="&messengerCmd.accesskey;"
+ label="&messengerCmd.label;" key="key_mail"
+ oncommand="toMessengerWindow();"/>
+ <menuitem id="tasksMenuAddressBook"
+ label="&addressBookCmd.label;"
+ accesskey="&addressBookCmd.accesskey;"
+ oncommand="toAddressBook();"/>
+#ifndef XP_MACOSX
+ <menuseparator id="prefSep"/>
+ <menuitem id="menu_accountmgr"
+ label="&accountManagerCmd2.label;"
+ accesskey="&accountManagerCmd2.accesskey;"
+ command="cmd_account"/>
+ <menuitem id="menu_preferences"
+ data-l10n-id="menu-tools-settings"
+ oncommand="openOptionsDialog('paneCompose');"/>
+#endif
+ </menupopup>
+ </menu>
+
+#ifdef XP_MACOSX
+#include ../../../base/content/macWindowMenu.inc.xhtml
+#endif
+
+ <!-- Help -->
+#include ../../../base/content/helpMenu.inc.xhtml
+ </menubar>
+ </toolbaritem>
+ </toolbar>
+
+ <toolbarpalette id="MsgComposeToolbarPalette">
+
+ <toolbarbutton class="toolbarbutton-1"
+ id="button-send" label="&sendButton.label;"
+ tooltiptext="&sendButton.tooltip;"
+ command="cmd_sendButton"
+ now_label="&sendButton.label;"
+ now_tooltiptext="&sendButton.tooltip;"
+ later_label="&sendLaterCmd.label;"
+ later_tooltiptext="&sendlaterButton.tooltip;">
+ </toolbarbutton>
+
+ <toolbarbutton class="toolbarbutton-1"
+ id="button-contacts" label="&addressButton.label;"
+ tooltiptext="&addressButton.tooltip;"
+ autoCheck="false" type="checkbox"
+ oncommand="toggleContactsSidebar();"/>
+
+ <toolbarbutton is="toolbarbutton-menu-button" id="button-attach"
+ data-l10n-id="toolbar-button-add-attachment"
+ type="menu"
+ class="toolbarbutton-1"
+ command="cmd_attachFile">
+ <menupopup id="button-attachPopup" onpopupshowing="updateAttachmentItems();">
+ <menuitem id="button-attachPopup_attachFileItem"
+ data-l10n-id="menuitem-attach-files"
+ data-l10n-attrs="acceltext"
+ command="cmd_attachFile"/>
+ <menu id="button-attachPopup_attachCloudMenu"
+ label="&attachCloudCmd.label;"
+ accesskey="&attachCloudCmd.accesskey;"
+ command="cmd_attachCloud">
+ <menupopup id="attachCloudMenu_popup" onpopupshowing="if (event.target == this) { addAttachCloudMenuItems(this); }"/>
+ </menu>
+ <menuitem id="button-attachPopup_attachPageItem"
+ label="&attachPageCmd.label;"
+ accesskey="&attachPageCmd.accesskey;"
+ command="cmd_attachPage"/>
+ <menuseparator/>
+ <menuitem id="button-attachPopup_attachVCardItem"
+ type="checkbox"
+ data-l10n-id="context-menuitem-attach-vcard"
+ command="cmd_attachVCard"/>
+ <menuitem id="button-attachPopup_attachPublicKey"
+ type="checkbox"
+ data-l10n-id="context-menuitem-attach-openpgp-key"
+ command="cmd_attachPublicKey"/>
+ <menuseparator id="button-attachPopup_remindLaterSeparator"/>
+ <menuitem id="button-attachPopup_remindLaterItem"
+ type="checkbox"
+ label="&remindLater.label;"
+ accesskey="&remindLater.accesskey;"
+ command="cmd_remindLater"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarbutton id="button-encryption"
+ type="checkbox" autoCheck="false"
+ class="toolbarbutton-1"
+ data-l10n-id="encryption-toggle"
+ oncommand="toggleEncryptMessage();"/>
+
+ <toolbarbutton id="button-signing"
+ type="checkbox" autoCheck="false"
+ class="toolbarbutton-1"
+ data-l10n-id="signing-toggle"
+ oncommand="toggleGlobalSignMessage();"/>
+
+ <toolbarbutton is="toolbarbutton-menu-button" id="button-encryption-options"
+ type="menu"
+ class="toolbarbutton-1"
+ data-l10n-id="encryption-options-openpgp"
+ oncommand="showPopupById('encryptionToolbarMenu', 'button-encryption-options');">
+ <menupopup id="encryptionToolbarMenu"
+ onpopupshowing="setSecuritySettings('_Toolbar');"
+ oncommand="onEncryptionChoice(event.target.value);">
+
+ <menuitem id="encTech_OpenPGP_Toolbar"
+ label="&menu_techPGP.label;" accesskey="&menu_techPGP.accesskey;"
+ value="OpenPGP" type="radio" name="radiogroup_encTech"/>
+ <menuitem id="encTech_SMIME_Toolbar"
+ label="&menu_techSMIME.label;" accesskey="&menu_techSMIME.accesskey;"
+ value="SMIME" type="radio" name="radiogroup_encTech"/>
+
+ <menuseparator id="encryptionOptionsSeparator_Toolbar"/>
+
+ <menuitem id="menu_securityEncrypt_Toolbar"
+ type="checkbox"
+ data-l10n-id="menu-encrypt"
+ value="enc"/>
+ <menuitem id="menu_securityEncryptSubject_Toolbar"
+ type="checkbox"
+ data-l10n-id="menu-encrypt-subject"
+ value="encsub"/>
+ <menuitem id="menu_securitySign_Toolbar"
+ type="checkbox"
+ data-l10n-id="menu-sign"
+ value="sig"/>
+
+ <menuseparator id="statusInfoSeparator"/>
+
+ <menuitem id="menu_recipientStatus_Toolbar"
+ data-l10n-id="menu-manage-keys"
+ value="status"/>
+ <menuitem id="menu_openManager_Toolbar"
+ data-l10n-id="menu-open-key-manager"
+ value="manager"/>
+
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarbutton is="toolbarbutton-menu-button" id="spellingButton"
+ type="menu"
+ class="toolbarbutton-1"
+ label="&spellingButton.label;"
+ tooltiptext="&spellingButton.tooltip;"
+ command="cmd_spelling">
+ <!-- workaround for the bug that split menu doesn't take popup="popupID" -->
+ <menupopup onpopupshowing="event.preventDefault();
+ showPopupById('languageMenuList',
+ 'spellingButton');"/>
+ </toolbarbutton>
+
+ <toolbarbutton is="toolbarbutton-menu-button" id="button-save"
+ type="menu"
+ class="toolbarbutton-1"
+ label="&saveButton.label;"
+ tooltiptext="&saveButton.tooltip;"
+ command="cmd_saveDefault">
+ <menupopup id="button-savePopup" onpopupshowing="InitFileSaveAsMenu();">
+ <menuitem id="savePopup_saveAsFile"
+ label="&saveAsFileCmd.label;" accesskey="&saveAsFileCmd.accesskey;"
+ command="cmd_saveAsFile" type="radio" name="radiogroup_SaveAs"/>
+ <menuseparator/>
+ <menuitem id="savePopup_saveAsDraft"
+ label="&saveAsDraftCmd.label;" accesskey="&saveAsDraftCmd.accesskey;"
+ command="cmd_saveAsDraft" type="radio" name="radiogroup_SaveAs"/>
+ <menuitem id="savePopup_saveAsTemplate"
+ label="&saveAsTemplateCmd.label;" accesskey="&saveAsTemplateCmd.accesskey;"
+ command="cmd_saveAsTemplate" type="radio" name="radiogroup_SaveAs"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarbutton id="button-print"
+ class="toolbarbutton-1"
+ label="&printButton.label;"
+ command="cmd_print"
+ tooltiptext="&printButton.tooltip;"/>
+ <toolbarbutton class="toolbarbutton-1"
+ id="quoteButton" label="&quoteButton.label;"
+ tooltiptext="&quoteButton.tooltip;"
+ command="cmd_quoteMessage"/>
+
+ <toolbarbutton id="cut-button" class="toolbarbutton-1"
+ data-l10n-id="text-action-cut"
+ command="cmd_cut"
+ tooltiptext="&cutButton.tooltip;"/>
+ <toolbarbutton id="copy-button" class="toolbarbutton-1"
+ data-l10n-id="text-action-copy"
+ command="cmd_copy"
+ tooltiptext="&copyButton.tooltip;"/>
+ <toolbarbutton id="paste-button" class="toolbarbutton-1"
+ data-l10n-id="text-action-paste"
+ command="cmd_paste"
+ tooltiptext="&pasteButton.tooltip;"/>
+
+ <toolbaritem id="priority-button"
+ align="center"
+ pack="center"
+ title="&priorityButton.title;"
+ tooltiptext="&priorityButton.tooltiptext;">
+ <label value="&priorityButton.label;" control="priorityMenu-button"/>
+ <menulist id="priorityMenu-button" value="" oncommand="PriorityMenuSelect(event.target);">
+ <menupopup id="priorityMenu-buttonPopup">
+ <menuitem id="list_priority_highest"
+ name="priority"
+ label="&highestPriorityCmd.label;"
+ value="Highest"/>
+ <menuitem id="list_priority_high"
+ name="priority"
+ label="&highPriorityCmd.label;"
+ value="High"/>
+ <menuitem id="list_priority_normal"
+ name="priority"
+ selected="true"
+ label="&normalPriorityCmd.label;"
+ value=""/>
+ <menuitem id="list_priority_low"
+ name="priority"
+ label="&lowPriorityCmd.label;"
+ value="Low"/>
+ <menuitem id="list_priority_lowest"
+ name="priority"
+ label="&lowestPriorityCmd.label;"
+ value="Lowest"/>
+ </menupopup>
+ </menulist>
+ </toolbaritem>
+
+ <toolbarbutton id="button-returnReceipt"
+ class="toolbarbutton-1"
+ data-l10n-id="button-return-receipt"
+ type="checkbox" autoCheck="false"
+ command="cmd_toggleReturnReceipt"/>
+ </toolbarpalette>
+ <toolbar is="customizable-toolbar"
+ id="composeToolbar2"
+ class="chromeclass-toolbar themeable-full"
+ toolbarname="&showCompositionToolbarCmd.label;"
+ accesskey="&showCompositionToolbarCmd.accesskey;"
+ fullscreentoolbar="true" mode="full"
+#ifdef XP_MACOSX
+ iconsize="small"
+#endif
+ defaultset="button-send,separator,button-encryption,button-encryption-options,button-address,spellingButton,button-save,button-contacts,spring,button-attach"
+ customizable="true"
+ context="toolbar-context-menu">
+ </toolbar>
+</toolbox>
+ <html:div id="composeContentBox" class="printPreviewStack attachment-area-hidden">
+ <html:div id="contactsSidebar">
+ <box class="sidebar-header" align="center">
+ <label id="contactsTitle" value="&addressesSidebarTitle.label;"/>
+ <spacer flex="1"/>
+ <toolbarbutton class="close-icon"
+ oncommand="toggleContactsSidebar();"/>
+ </box>
+ <browser id="contactsBrowser" src="" disablehistory="true"/>
+ </html:div>
+
+ <html:hr is="pane-splitter" id="contactsSplitter"
+ resize-direction="horizontal"
+ resize-id="contactsSidebar" />
+
+ <toolbar is="customizable-toolbar" id="MsgHeadersToolbar"
+ class="themeable-full"
+ customizable="true" nowindowdrag="true"
+ ondragover="envelopeDragObserver.onDragOver(event);"
+ ondrop="envelopeDragObserver.onDrop(event);"
+ ondragleave="envelopeDragObserver.onDragLeave(event);">
+ <hbox id="top-gradient-box" class="address-identity-recipient">
+ <hbox class="aw-firstColBox"/>
+ <hbox id="identityLabel-box" align="center"
+ pack="end" style="&headersSpace2.style;">
+ <label id="identityLabel" value="&fromAddr2.label;"
+ accesskey="&fromAddr.accesskey;" control="msgIdentity"/>
+ </hbox>
+ <menulist is="menulist-editable" id="msgIdentity"
+ type="description"
+ disableautoselect="true" onkeypress="fromKeyPress(event);"
+ oncommand="LoadIdentity(false);" disableonsend="true">
+ <menupopup id="msgIdentityPopup"/>
+ </menulist>
+
+ <html:div id="extraAddressRowsArea">
+ <!-- Default set up is for a mail account, where we prefer
+ - showing the buttons, rather than the menu items, for
+ - the mail rows.
+ - The To field is already shown, so the button is hidden.
+ - For the news rows, we prefer the menu items over the
+ - buttons, so we hide them. -->
+ <html:button id="addr_toShowAddressRowButton"
+ disableonsend="true"
+ class="recipient-button plain-button"
+ data-address-row="addressRowTo"
+ onclick="showAndFocusAddressRow('addressRowTo');"
+ ondrop="showAddressRowButtonOnDrop(event);"
+ ondragover="showAddressRowButtonOnDragover(event);"
+ hidden="hidden">
+ </html:button>
+ <html:button id="addr_ccShowAddressRowButton"
+ disableonsend="true"
+ class="recipient-button plain-button"
+ data-address-row="addressRowCc"
+ onclick="showAndFocusAddressRow('addressRowCc');"
+ ondrop="showAddressRowButtonOnDrop(event);"
+ ondragover="showAddressRowButtonOnDragover(event);">
+ </html:button>
+ <html:button id="addr_bccShowAddressRowButton"
+ disableonsend="true"
+ class="recipient-button plain-button"
+ data-address-row="addressRowBcc"
+ onclick="showAndFocusAddressRow('addressRowBcc');"
+ ondrop="showAddressRowButtonOnDrop(event);"
+ ondragover="showAddressRowButtonOnDragover(event);">
+ </html:button>
+ <html:button id="addr_newsgroupsShowAddressRowButton"
+ class="recipient-button plain-button"
+ hidden="hidden"
+ onclick="showAndFocusAddressRow('addressRowNewsgroups')">
+ &newsgroupsAddr2.label;
+ </html:button>
+ <html:button id="addr_followupShowAddressRowButton"
+ class="recipient-button plain-button"
+ hidden="hidden"
+ onclick="showAndFocusAddressRow('addressRowFollowup')">
+ &followupAddr2.label;
+ </html:button>
+ <html:button id="extraAddressRowsMenuButton"
+ data-l10n-id="extra-address-rows-menu-button"
+ aria-expanded="false"
+ aria-haspopup="menu"
+ aria-controls="extraAddressRowsMenu"
+ disableonsend="true"
+ class="plain-button"
+ onclick="openExtraAddressRowsMenu();">
+ <!-- NOTE: button title should provide the accessibility
+ - context. -->
+ <html:img class="overflow-icon"
+ src="chrome://messenger/skin/icons/new/compact/overflow.svg"
+ alt="" />
+ </html:button>
+ </html:div>
+ </hbox>
+
+ <mail-recipients-area id="recipientsContainer" orient="vertical"
+ class="recipients-container">
+ <hbox id="addressRowReply"
+ class="address-row hidden"
+ data-recipienttype="addr_reply"
+ data-show-self-menuitem="addr_replyShowAddressRowMenuItem">
+ <hbox class="aw-firstColBox">
+ <html:button class="remove-field-button plain-button"
+ onclick="closeLabelOnClick(event);">
+ <html:img src="chrome://global/skin/icons/close.svg"
+ alt="" />
+ </html:button>
+ </hbox>
+ <hbox class="address-label-container" align="top" pack="end"
+ style="&headersSpace2.style;">
+ <label id="replyAddrLabel" value="&replyAddr2.label;"
+ control="replyAddrInput"/>
+ </hbox>
+ <hbox id="replyAddrContainer" flex="1" align="center"
+ class="input-container wrap-container address-container"
+ onclick="focusAddressInputOnClick(event);">
+ <html:input is="autocomplete-input" id="replyAddrInput"
+ type="text"
+ class="plain address-input address-row-input mail-input"
+ disableonsend="true"
+ autocompletesearch="mydomain addrbook ldap news"
+ autocompletesearchparam="{}"
+ timeout="200"
+ maxrows="6"
+ completedefaultindex="true"
+ forcecomplete="true"
+ completeselectedindex="true"
+ minresultsforpopup="2"
+ ignoreblurwhilesearching="true"
+ onfocus="addressInputOnFocus(this);"
+ onblur="addressInputOnBlur(this);"
+ size="1"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="addressRowTo"
+ class="address-row"
+ data-recipienttype="addr_to"
+ data-show-self-menuitem="addr_toShowAddressRowMenuItem">
+ <hbox class="aw-firstColBox">
+ <html:button class="remove-field-button plain-button"
+ onclick="closeLabelOnClick(event);"
+ hidden="hidden">
+ <html:img src="chrome://global/skin/icons/close.svg"
+ alt="" />
+ </html:button>
+ </hbox>
+ <hbox class="address-label-container" align="top" pack="end"
+ style="&headersSpace2.style;">
+ <label id="toAddrLabel"
+ data-l10n-id="to-address-row-label"
+ control="toAddrInput"/>
+ </hbox>
+ <hbox id="toAddrContainer" flex="1" align="center"
+ class="input-container wrap-container address-container"
+ onclick="focusAddressInputOnClick(event);">
+ <html:input is="autocomplete-input" id="toAddrInput"
+ type="text"
+ class="plain address-input address-row-input mail-input mail-primary-input"
+ disableonsend="true"
+ autocompletesearch="mydomain addrbook ldap news"
+ autocompletesearchparam="{}"
+ timeout="200"
+ maxrows="6"
+ completedefaultindex="true"
+ forcecomplete="true"
+ completeselectedindex="true"
+ minresultsforpopup="2"
+ ignoreblurwhilesearching="true"
+ onfocus="addressInputOnFocus(this);"
+ onblur="addressInputOnBlur(this);"
+ size="1"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="addressRowCc"
+ class="address-row hidden"
+ data-recipienttype="addr_cc"
+ data-show-self-menuitem="addr_ccShowAddressRowMenuItem">
+ <hbox class="aw-firstColBox">
+ <html:button class="remove-field-button plain-button"
+ onclick="closeLabelOnClick(event);">
+ <html:img src="chrome://global/skin/icons/close.svg"
+ alt="" />
+ </html:button>
+ </hbox>
+ <hbox class="address-label-container" align="top" pack="end"
+ style="&headersSpace2.style;">
+ <label id="ccAddrLabel"
+ data-l10n-id="cc-address-row-label"
+ control="ccAddrInput"/>
+ </hbox>
+ <hbox id="ccAddrContainer" flex="1" align="center"
+ class="input-container wrap-container address-container"
+ onclick="focusAddressInputOnClick(event);">
+ <html:input is="autocomplete-input" id="ccAddrInput"
+ type="text"
+ class="plain address-input address-row-input mail-input"
+ disableonsend="true"
+ autocompletesearch="mydomain addrbook ldap news"
+ autocompletesearchparam="{}"
+ timeout="200"
+ maxrows="6"
+ completedefaultindex="true"
+ forcecomplete="true"
+ completeselectedindex="true"
+ minresultsforpopup="2"
+ ignoreblurwhilesearching="true"
+ onfocus="addressInputOnFocus(this);"
+ onblur="addressInputOnBlur(this);"
+ size="1"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="addressRowBcc"
+ class="address-row hidden"
+ data-recipienttype="addr_bcc"
+ data-show-self-menuitem="addr_bccShowAddressRowMenuItem">
+ <hbox class="aw-firstColBox">
+ <html:button class="remove-field-button plain-button"
+ onclick="closeLabelOnClick(event);">
+ <html:img src="chrome://global/skin/icons/close.svg"
+ alt="" />
+ </html:button>
+ </hbox>
+ <hbox class="address-label-container" align="top" pack="end"
+ style="&headersSpace2.style;">
+ <label id="bccAddrLabel"
+ data-l10n-id="bcc-address-row-label"
+ control="bccAddrInput"/>
+ </hbox>
+ <hbox id="bccAddrContainer" flex="1" align="center"
+ class="input-container wrap-container address-container"
+ onclick="focusAddressInputOnClick(event);">
+ <html:input is="autocomplete-input" id="bccAddrInput"
+ type="text"
+ class="plain address-input address-row-input mail-input"
+ disableonsend="true"
+ autocompletesearch="mydomain addrbook ldap news"
+ autocompletesearchparam="{}"
+ timeout="200"
+ maxrows="6"
+ completedefaultindex="true"
+ forcecomplete="true"
+ completeselectedindex="true"
+ minresultsforpopup="2"
+ ignoreblurwhilesearching="true"
+ onfocus="addressInputOnFocus(this);"
+ onblur="addressInputOnBlur(this);"
+ size="1"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="addressRowNewsgroups"
+ class="address-row hidden"
+ data-recipienttype="addr_newsgroups"
+ data-show-self-menuitem="addr_newsgroupsShowAddressRowMenuItem">
+ <hbox class="aw-firstColBox">
+ <html:button class="remove-field-button plain-button"
+ onclick="closeLabelOnClick(event);">
+ <html:img src="chrome://global/skin/icons/close.svg"
+ alt="" />
+ </html:button>
+ </hbox>
+ <hbox class="address-label-container" align="top" pack="end"
+ style="&headersSpace2.style;">
+ <label id="newsgroupsAddrLabel" value="&newsgroupsAddr2.label;"
+ control="newsgroupsAddrInput"/>
+ </hbox>
+ <hbox id="newsgroupsAddrContainer" flex="1" align="center"
+ class="input-container wrap-container address-container"
+ onclick="focusAddressInputOnClick(event);">
+ <html:input is="autocomplete-input" id="newsgroupsAddrInput"
+ type="text"
+ class="plain address-input address-row-input news-input news-primary-input"
+ disableonsend="true"
+ autocompletesearch="mydomain addrbook ldap news"
+ autocompletesearchparam="{}"
+ timeout="200"
+ maxrows="6"
+ completedefaultindex="true"
+ forcecomplete="true"
+ completeselectedindex="true"
+ minresultsforpopup="2"
+ ignoreblurwhilesearching="true"
+ onfocus="addressInputOnFocus(this);"
+ onblur="addressInputOnBlur(this);"
+ size="1"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="addressRowFollowup"
+ class="address-row hidden"
+ data-recipienttype="addr_followup"
+ data-show-self-menuitem="addr_followupShowAddressRowMenuItem">
+ <hbox class="aw-firstColBox">
+ <html:button class="remove-field-button plain-button"
+ onclick="closeLabelOnClick(event);">
+ <html:img src="chrome://global/skin/icons/close.svg"
+ alt="" />
+ </html:button>
+ </hbox>
+ <hbox class="address-label-container" align="top" pack="end"
+ style="&headersSpace2.style;">
+ <label id="followupAddrLabel" value="&followupAddr2.label;"
+ control="followupAddrInput"/>
+ </hbox>
+ <hbox id="followupAddrContainer" flex="1" align="center"
+ class="input-container wrap-container address-container"
+ onclick="focusAddressInputOnClick(event);">
+ <html:input is="autocomplete-input" id="followupAddrInput"
+ type="text"
+ class="plain address-input address-row-input news-input"
+ disableonsend="true"
+ autocompletesearch="mydomain addrbook ldap news"
+ autocompletesearchparam="{}"
+ timeout="200"
+ maxrows="6"
+ completedefaultindex="true"
+ forcecomplete="true"
+ completeselectedindex="true"
+ minresultsforpopup="2"
+ ignoreblurwhilesearching="true"
+ onfocus="addressInputOnFocus(this);"
+ onblur="addressInputOnBlur(this);"
+ size="1"/>
+ </hbox>
+ </hbox>
+ </mail-recipients-area>
+
+ <hbox id="subject-box">
+ <hbox class="aw-firstColBox"/>
+ <hbox id="subjectLabel-box" align="center"
+ pack="end" style="&headersSpace2.style;">
+ <label id="subjectLabel" value="&subject2.label;"
+ accesskey="&subject.accesskey;" control="msgSubject"/>
+ </hbox>
+ <hbox id="msgSubjectContainer" flex="1" align="center"
+ class="input-container">
+ <moz-input-box spellcheck="true" style="flex: 1;">
+ <html:img id="msgEncryptedSubjectIcon"
+ src="chrome://messenger/skin/icons/message-encrypted-notok.svg"
+ onclick="toggleEncryptedSubject(event);"
+ hidden="hidden"
+ alt="" />
+ <html:input id="msgSubject"
+ type="text"
+ class="input-inline textbox-input"
+ disableonsend="true"
+ oninput="msgSubjectOnInput(event);"
+ onkeypress="subjectKeyPress(event);"
+ aria-labelledby="subjectLabel"
+ style="flex: 1;"/>
+ </moz-input-box>
+ </hbox>
+ </hbox>
+ </toolbar>
+
+ <toolbox id="FormatToolbox" mode="icons">
+ <toolbar id="FormatToolbar"
+ class="chromeclass-toolbar themeable-brighttext"
+ persist="collapsed"
+ nowindowdrag="true">
+#include editFormatButtons.inc.xhtml
+ <spacer flex="1"/>
+ </toolbar>
+ </toolbox>
+
+ <html:hr is="pane-splitter" id="headersSplitter"
+ resize-direction="vertical"
+ resize-id="MsgHeadersToolbar" />
+ <html:div id="messageArea">
+ <html:div id="dropAttachmentOverlay" class="drop-attachment-overlay">
+ <html:aside id="addInline" class="drop-attachment-box">
+ <html:span id="addInlineLabel"
+ data-l10n-id="drop-file-label-inline"
+ data-l10n-args='{"count": 1}'
+ class="drop-inline"></html:span>
+ </html:aside>
+ <html:aside id="addAsAttachment"
+ class="drop-attachment-box">
+ <html:span id="addAsAttachmentLabel"
+ data-l10n-id="drop-file-label-attachment"
+ data-l10n-args='{"count": 1}'
+ class="drop-as-attachment"></html:span>
+ </html:aside>
+ </html:div>
+ <!--
+ - The mail message body frame. The src does not exactly match
+ - "about:blank" so that WebExtension content scripts are not loaded
+ - here in the moments before navigation to about:blank?compose occurs.
+ -->
+ <editor id="messageEditor"
+ type="content"
+ primary="true"
+ src="about:blank?"
+ name="browser.message.body"
+ aria-label="&aria.message.bodyName;"
+ messagemanagergroup="browsers"
+ oncontextmenu="this._contextX = event.pageX; this._contextY = event.pageY;"
+ onclick="EditorClick(event);"
+ ondblclick="EditorDblClick(event);"
+ context="msgComposeContext"/>
+
+ <html:div id="linkPreviewSettings" xmlns="http://www.w3.org/1999/xhtml" hidden="hidden">
+ <span class="close">+</span>
+ <h2 data-l10n-id="link-preview-title"></h2>
+ <p data-l10n-id="link-preview-description"></p>
+ <p>
+ <input class="preview-autoadd" id="link-preview-autoadd" type="checkbox" />
+ <label data-l10n-id="link-preview-autoadd" for="link-preview-autoadd"></label>
+ </p>
+ <p class="bottom">
+ <span data-l10n-id="link-preview-replace-now"></span>
+ <button class="preview-replace" data-l10n-id="link-preview-yes-replace"></button>
+ </p>
+ </html:div>
+
+ <findbar id="FindToolbar" browserid="messageEditor"/>
+ </html:div>
+
+ <!-- NOTE: The splitter controls #attachmentBucket's size directly. -->
+ <html:hr is="pane-splitter" id="attachmentSplitter"
+ resize-direction="vertical"
+ resize-id="attachmentBucket" />
+ <html:details id="attachmentArea">
+ <html:summary>
+ <!-- Hide from accessibility tree since this is only used for a brief
+ - animation effect. -->
+ <html:span id="newAttachmentIndicator" aria-hidden="true"></html:span>
+ <html:img id="attachmentToggle"
+ src="chrome://messenger/skin/icons/new/nav-down-sm.svg"
+ alt="" />
+ <html:span id="attachmentBucketCount"></html:span>
+ <html:span id="attachmentBucketSize" role="note"></html:span>
+ </html:summary>
+
+ <richlistbox is="attachment-list" id="attachmentBucket"
+ aria-describedby="attachmentBucketCount"
+ class="attachmentList"
+ disableonsend="true"
+ seltype="multiple"
+ flex="1"
+ role="listbox"
+ context="msgComposeAttachmentListContext"
+ itemcontext="msgComposeAttachmentItemContext"
+ onclick="attachmentBucketOnClick(event);"
+ onkeypress="attachmentBucketOnKeyPress(event);"
+ onselect="attachmentBucketOnSelect();"
+ ondragstart="attachmentBucketDNDObserver.onDragStart(event);"
+ ondragover="envelopeDragObserver.onDragOver(event);"
+ ondrop="envelopeDragObserver.onDrop(event);"
+ ondragleave="envelopeDragObserver.onDragLeave(event);"
+ onblur="attachmentBucketOnBlur();"/>
+ </html:details>
+ </html:div>
+
+ <panel id="customizeToolbarSheetPopup" noautohide="true">
+ <iframe id="customizeToolbarSheetIFrame"
+ style="&dialog.dimensions;"
+ hidden="true"/>
+ </panel>
+
+ <vbox id="compose-notification-bottom">
+ <!-- notificationbox will be added here lazily. -->
+ </vbox>
+
+ <html:div id="status-bar" class="statusbar" role="status">
+ <html:div id="statusText"></html:div>
+ <html:progress id="compose-progressmeter"
+ class="progressmeter-statusbar"
+ value="0" max="100"
+ hidden="hidden">
+ </html:progress>
+ <html:button id="languageStatusButton"
+ class="plain-button"
+ aria-expanded="false"
+ aria-haspopup="menu"
+ aria-controls="languageMenuList"
+ title="&languageStatusButton.tooltip;"
+ onclick="showPopupById('languageMenuList', 'languageStatusButton', 'before_start');"
+ hidden="hidden">
+ </html:button>
+ </html:div>
+
+#include ../../../base/content/tabDialogs.inc.xhtml
+#include ../../../extensions/openpgp/content/ui/keyAssistant.inc.xhtml
+
+<html:template id="dataCardTemplate" xmlns="http://www.w3.org/1999/xhtml">
+ <aside class="moz-card" style="width:600px; display:flex; align-items:center; justify-content:center; flex-direction:row; flex-wrap:wrap; border-radius:10px; border:1px solid silver;">
+ <a class="remove-card">+</a>
+ <div class="card-pic" style="display:flex; flex-direction:column; flex-basis:100%; flex:1;">
+ <div style="margin:0 5px;">
+ <img src="IMAGE" style="width:120px;" alt="" />
+ </div>
+ </div>
+ <div class="card-content" style="display:flex; flex-direction:column; flex-basis:100%; flex:3;">
+ <div style="margin:0 1em;">
+ <p><small class="site" style="font-weight:lighter;">SITE</small></p>
+ <p>
+ <a href="#" style="font-weight:600; text-decoration:none;"><big class="title">TITLE</big></a>
+ </p>
+ <p class="description">DESCRIPTION</p>
+ <p>
+ <a href="#" class="url" style="display:inline-block; text-decoration:none; text-indent:-2ch; margin-inline:2ch;">URL</a>
+ </p>
+ </div>
+ </div>
+ </aside>
+</html:template>
+</html:body>
+</html>