/* 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 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 . 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 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 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(); } } }