diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/compose/content/editor.js | |
parent | Initial commit. (diff) | |
download | thunderbird-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/editor.js')
-rw-r--r-- | comm/mail/components/compose/content/editor.js | 2392 |
1 files changed, 2392 insertions, 0 deletions
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(); + } + } +} |