summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/compose/content/editor.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/compose/content/editor.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/compose/content/editor.js')
-rw-r--r--comm/mail/components/compose/content/editor.js2392
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();
+ }
+ }
+}