diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/compose/content/dialogs/EdDialogCommon.js | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/compose/content/dialogs/EdDialogCommon.js')
-rw-r--r-- | comm/mail/components/compose/content/dialogs/EdDialogCommon.js | 679 |
1 files changed, 679 insertions, 0 deletions
diff --git a/comm/mail/components/compose/content/dialogs/EdDialogCommon.js b/comm/mail/components/compose/content/dialogs/EdDialogCommon.js new file mode 100644 index 0000000000..ce377e4bbf --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdDialogCommon.js @@ -0,0 +1,679 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Each editor window must include this file + +/* import-globals-from ../editorUtilities.js */ +/* globals InitDialog, ChangeLinkLocation, ValidateData */ + +// Object to attach commonly-used widgets (all dialogs should use this) +var gDialog = {}; + +var gHaveDocumentUrl = false; +var gValidationError = false; + +// Use for 'defaultIndex' param in InitPixelOrPercentMenulist +const gPixel = 0; +const gPercent = 1; + +const gMaxPixels = 100000; // Used for image size, borders, spacing, and padding +// Gecko code uses 1000 for maximum rowspan, colspan +// Also, editing performance is really bad above this +const gMaxRows = 1000; +const gMaxColumns = 1000; +const gMaxTableSize = 1000000; // Width or height of table or cells + +// A XUL element with id="location" for managing +// dialog location relative to parent window +var gLocation; + +// The element being edited - so AdvancedEdit can have access to it +var globalElement; + +/* Validate contents of an input field + * + * inputWidget The 'input' element for the the attribute's value + * listWidget The 'menulist' XUL element for choosing "pixel" or "percent" + * May be null when no pixel/percent is used. + * minVal minimum allowed for input widget's value + * maxVal maximum allowed for input widget's value + * (when "listWidget" is used, maxVal is used for "pixel" maximum, + * 100% is assumed if "percent" is the user's choice) + * element The DOM element that we set the attribute on. May be null. + * attName Name of the attribute to set. May be null or ignored if "element" is null + * mustHaveValue If true, error dialog is displayed if "value" is empty string + * + * This calls "ValidateNumberRange()", which puts up an error dialog to inform the user. + * If error, we also: + * Shift focus and select contents of the inputWidget, + * Switch to appropriate panel of tabbed dialog if user implements "SwitchToValidate()", + * and/or will expand the dialog to full size if "More / Fewer" feature is implemented + * + * Returns the "value" as a string, or "" if error or input contents are empty + * The global "gValidationError" variable is set true if error was found + */ +function ValidateNumber( + inputWidget, + listWidget, + minVal, + maxVal, + element, + attName, + mustHaveValue +) { + if (!inputWidget) { + gValidationError = true; + return ""; + } + + // Global error return value + gValidationError = false; + var maxLimit = maxVal; + var isPercent = false; + + var numString = TrimString(inputWidget.value); + if (numString || mustHaveValue) { + if (listWidget) { + isPercent = listWidget.selectedIndex == 1; + } + if (isPercent) { + maxLimit = 100; + } + + // This method puts up the error message + numString = ValidateNumberRange(numString, minVal, maxLimit, mustHaveValue); + if (!numString) { + // Switch to appropriate panel for error reporting + SwitchToValidatePanel(); + + // Error - shift to offending input widget + SetTextboxFocus(inputWidget); + gValidationError = true; + } else { + if (isPercent) { + numString += "%"; + } + if (element) { + GetCurrentEditor().setAttributeOrEquivalent( + element, + attName, + numString, + true + ); + } + } + } else if (element) { + GetCurrentEditor().removeAttributeOrEquivalent(element, attName, true); + } + return numString; +} + +/* Validate contents of an input field + * + * value number to validate + * minVal minimum allowed for input widget's value + * maxVal maximum allowed for input widget's value + * (when "listWidget" is used, maxVal is used for "pixel" maximum, + * 100% is assumed if "percent" is the user's choice) + * mustHaveValue If true, error dialog is displayed if "value" is empty string + * + * If inputWidget's value is outside of range, or is empty when "mustHaveValue" = true, + * an error dialog is popuped up to inform the user. The focus is shifted + * to the inputWidget. + * + * Returns the "value" as a string, or "" if error or input contents are empty + * The global "gValidationError" variable is set true if error was found + */ +function ValidateNumberRange(value, minValue, maxValue, mustHaveValue) { + // Initialize global error flag + gValidationError = false; + value = TrimString(String(value)); + + // We don't show error for empty string unless caller wants to + if (!value && !mustHaveValue) { + return ""; + } + + var numberStr = ""; + + if (value.length > 0) { + // Extract just numeric characters + var number = Number(value.replace(/\D+/g, "")); + if (number >= minValue && number <= maxValue) { + // Return string version of the number + return String(number); + } + numberStr = String(number); + } + + var message = ""; + + if (numberStr.length > 0) { + // We have a number from user outside of allowed range + message = GetString("ValidateRangeMsg"); + message = message.replace(/%n%/, numberStr); + message += "\n "; + } + message += GetString("ValidateNumberMsg"); + + // Replace variable placeholders in message with number values + message = message.replace(/%min%/, minValue).replace(/%max%/, maxValue); + ShowInputErrorMessage(message); + + // Return an empty string to indicate error + gValidationError = true; + return ""; +} + +function SetTextboxFocusById(id) { + SetTextboxFocus(document.getElementById(id)); +} + +function SetTextboxFocus(input) { + if (input) { + input.focus(); + } +} + +function ShowInputErrorMessage(message) { + Services.prompt.alert(window, GetString("InputError"), message); + window.focus(); +} + +// Get the text appropriate to parent container +// to determine what a "%" value is referring to. +// elementForAtt is element we are actually setting attributes on +// (a temporary copy of element in the doc to allow canceling), +// but elementInDoc is needed to find parent context in document +function GetAppropriatePercentString(elementForAtt, elementInDoc) { + var editor = GetCurrentEditor(); + try { + var name = elementForAtt.nodeName.toLowerCase(); + if (name == "td" || name == "th") { + return GetString("PercentOfTable"); + } + + // Check if element is within a table cell + if (editor.getElementOrParentByTagName("td", elementInDoc)) { + return GetString("PercentOfCell"); + } + return GetString("PercentOfWindow"); + } catch (e) { + return ""; + } +} + +function ClearListbox(listbox) { + if (listbox) { + listbox.clearSelection(); + while (listbox.hasChildNodes()) { + listbox.lastChild.remove(); + } + } +} + +function forceInteger(elementID) { + var editField = document.getElementById(elementID); + if (!editField) { + return; + } + + var stringIn = editField.value; + if (stringIn && stringIn.length > 0) { + // Strip out all nonnumeric characters + stringIn = stringIn.replace(/\D+/g, ""); + if (!stringIn) { + stringIn = ""; + } + + // Write back only if changed + if (stringIn != editField.value) { + editField.value = stringIn; + } + } +} + +function InitPixelOrPercentMenulist( + elementForAtt, + elementInDoc, + attribute, + menulistID, + defaultIndex +) { + if (!defaultIndex) { + defaultIndex = gPixel; + } + + // var size = elementForAtt.getAttribute(attribute); + var size = GetHTMLOrCSSStyleValue(elementForAtt, attribute, attribute); + var menulist = document.getElementById(menulistID); + var pixelItem; + var percentItem; + + if (!menulist) { + dump("NO MENULIST found for ID=" + menulistID + "\n"); + return size; + } + + menulist.removeAllItems(); + pixelItem = menulist.appendItem(GetString("Pixels")); + + if (!pixelItem) { + return 0; + } + + percentItem = menulist.appendItem( + GetAppropriatePercentString(elementForAtt, elementInDoc) + ); + if (size && size.length > 0) { + // Search for a "%" or "px" + if (size.includes("%")) { + // Strip out the % + size = size.substr(0, size.indexOf("%")); + if (percentItem) { + menulist.selectedItem = percentItem; + } + } else { + if (size.includes("px")) { + // Strip out the px + size = size.substr(0, size.indexOf("px")); + } + menulist.selectedItem = pixelItem; + } + } else { + menulist.selectedIndex = defaultIndex; + } + + return size; +} + +function onAdvancedEdit() { + // First validate data from widgets in the "simpler" property dialog + if (ValidateData()) { + // Set true if OK is clicked in the Advanced Edit dialog + window.AdvancedEditOK = false; + // Open the AdvancedEdit dialog, passing in the element to be edited + // (the copy named "globalElement") + window.openDialog( + "chrome://messenger/content/messengercompose/EdAdvancedEdit.xhtml", + "_blank", + "chrome,close,titlebar,modal,resizable=yes", + "", + globalElement + ); + window.focus(); + if (window.AdvancedEditOK) { + // Copy edited attributes to the dialog widgets: + InitDialog(); + } + } +} + +function getColor(ColorPickerID) { + var colorPicker = document.getElementById(ColorPickerID); + var color; + if (colorPicker) { + // Extract color from colorPicker and assign to colorWell. + color = colorPicker.getAttribute("color"); + if (color && color == "") { + return null; + } + // Clear color so next if it's called again before + // color picker is actually used, we dedect the "don't set color" state + colorPicker.setAttribute("color", ""); + } + + return color; +} + +function setColorWell(ColorWellID, color) { + var colorWell = document.getElementById(ColorWellID); + if (colorWell) { + if (!color || color == "") { + // Don't set color (use default) + // Trigger change to not show color swatch + colorWell.setAttribute("default", "true"); + // Style in CSS sets "background-color", + // but color won't clear unless we do this: + colorWell.removeAttribute("style"); + } else { + colorWell.removeAttribute("default"); + // Use setAttribute so colorwell can be a XUL element, such as button + colorWell.setAttribute("style", "background-color:" + color); + } + } +} + +function SwitchToValidatePanel() { + // no default implementation + // Only EdTableProps.js currently implements this +} + +/** + * @returns {Promise} URL spec of the file chosen, or null + */ +function GetLocalFileURL(filterType) { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + var fileType = "html"; + + if (filterType == "img") { + fp.init(window, GetString("SelectImageFile"), Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterImages); + fileType = "image"; + } else if (filterType.startsWith("html")) { + // Current usage of this is in Link dialog, + // where we always want HTML first + fp.init(window, GetString("OpenHTMLFile"), Ci.nsIFilePicker.modeOpen); + + // When loading into Composer, direct user to prefer HTML files and text files, + // so we call separately to control the order of the filter list + fp.appendFilters(Ci.nsIFilePicker.filterHTML); + fp.appendFilters(Ci.nsIFilePicker.filterText); + + // Link dialog also allows linking to images + if (filterType.includes("img", 1)) { + fp.appendFilters(Ci.nsIFilePicker.filterImages); + } + } + // Default or last filter is "All Files" + fp.appendFilters(Ci.nsIFilePicker.filterAll); + + // set the file picker's current directory to last-opened location saved in prefs + SetFilePickerDirectory(fp, fileType); + + return new Promise(resolve => { + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK || !fp.file) { + resolve(null); + return; + } + SaveFilePickerDirectory(fp, fileType); + resolve(fp.fileURL.spec); + }); + }); +} + +function SetWindowLocation() { + gLocation = document.getElementById("location"); + if (gLocation) { + const screenX = Math.max( + 0, + Math.min( + window.opener.screenX + Number(gLocation.getAttribute("offsetX")), + screen.availWidth - window.outerWidth + ) + ); + const screenY = Math.max( + 0, + Math.min( + window.opener.screenY + Number(gLocation.getAttribute("offsetY")), + screen.availHeight - window.outerHeight + ) + ); + window.moveTo(screenX, screenY); + } +} + +function SaveWindowLocation() { + if (gLocation) { + gLocation.setAttribute("offsetX", window.screenX - window.opener.screenX); + gLocation.setAttribute("offsetY", window.screenY - window.opener.screenY); + } +} + +function onCancel() { + SaveWindowLocation(); +} + +function SetRelativeCheckbox(checkbox) { + if (!checkbox) { + checkbox = document.getElementById("MakeRelativeCheckbox"); + if (!checkbox) { + return; + } + } + + var editor = GetCurrentEditor(); + // Mail never allows relative URLs, so hide the checkbox + if (editor && editor.flags & Ci.nsIEditor.eEditorMailMask) { + checkbox.collapsed = true; + return; + } + + var input = document.getElementById(checkbox.getAttribute("for")); + if (!input) { + return; + } + + var url = TrimString(input.value); + var urlScheme = GetScheme(url); + + // Check it if url is relative (no scheme). + checkbox.checked = url.length > 0 && !urlScheme; + + // Now do checkbox enabling: + var enable = false; + + var docUrl = GetDocumentBaseUrl(); + var docScheme = GetScheme(docUrl); + + if (url && docUrl && docScheme) { + if (urlScheme) { + // Url is absolute + // If we can make a relative URL, then enable must be true! + // (this lets the smarts of MakeRelativeUrl do all the hard work) + enable = GetScheme(MakeRelativeUrl(url)).length == 0; + } else if (url[0] == "#") { + // Url is relative + // Check if url is a named anchor + // but document doesn't have a filename + // (it's probably "index.html" or "index.htm", + // but we don't want to allow a malformed URL) + var docFilename = GetFilename(docUrl); + enable = docFilename.length > 0; + } else { + // Any other url is assumed + // to be ok to try to make absolute + enable = true; + } + } + + SetElementEnabled(checkbox, enable); +} + +// oncommand handler for the Relativize checkbox in EditorOverlay.xhtml +function MakeInputValueRelativeOrAbsolute(checkbox) { + var input = document.getElementById(checkbox.getAttribute("for")); + if (!input) { + return; + } + + var docUrl = GetDocumentBaseUrl(); + if (!docUrl) { + // Checkbox should be disabled if not saved, + // but keep this error message in case we change that + Services.prompt.alert(window, "", GetString("SaveToUseRelativeUrl")); + window.focus(); + } else { + // Note that "checked" is opposite of its last state, + // which determines what we want to do here + if (checkbox.checked) { + input.value = MakeRelativeUrl(input.value); + } else { + input.value = MakeAbsoluteUrl(input.value); + } + + // Reset checkbox to reflect url state + SetRelativeCheckbox(checkbox); + } +} + +var IsBlockParent = [ + "applet", + "blockquote", + "body", + "center", + "dd", + "div", + "form", + "li", + "noscript", + "object", + "td", + "th", +]; + +var NotAnInlineParent = [ + "col", + "colgroup", + "dl", + "dir", + "menu", + "ol", + "table", + "tbody", + "tfoot", + "thead", + "tr", + "ul", +]; + +function FillLinkMenulist(linkMenulist, headingsArray) { + var editor = GetCurrentEditor(); + try { + var treeWalker = editor.document.createTreeWalker( + editor.document, + 1, + null, + true + ); + var headingList = []; + var anchorList = []; // for sorting + var anchorMap = {}; // for weeding out duplicates and making heading anchors unique + var anchor; + var i; + for ( + var element = treeWalker.nextNode(); + element; + element = treeWalker.nextNode() + ) { + // grab headings + // Skip headings that already have a named anchor as their first child + // (this may miss nearby anchors, but at least we don't insert another + // under the same heading) + if ( + HTMLHeadingElement.isInstance(element) && + element.textContent && + !( + HTMLAnchorElement.isInstance(element.firstChild) && + element.firstChild.name + ) + ) { + headingList.push(element); + } + + // grab named anchors + if (HTMLAnchorElement.isInstance(element) && element.name) { + anchor = "#" + element.name; + if (!(anchor in anchorMap)) { + anchorList.push({ anchor, sortkey: anchor.toLowerCase() }); + anchorMap[anchor] = true; + } + } + + // grab IDs + if (element.id) { + anchor = "#" + element.id; + if (!(anchor in anchorMap)) { + anchorList.push({ anchor, sortkey: anchor.toLowerCase() }); + anchorMap[anchor] = true; + } + } + } + // add anchor for headings + for (i = 0; i < headingList.length; i++) { + var heading = headingList[i]; + + // Use just first 40 characters, don't add "...", + // and replace whitespace with "_" and strip non-word characters + anchor = + "#" + + ConvertToCDATAString( + TruncateStringAtWordEnd(heading.textContent, 40, false) + ); + + // Append "_" to any name already in the list + while (anchor in anchorMap) { + anchor += "_"; + } + anchorList.push({ anchor, sortkey: anchor.toLowerCase() }); + anchorMap[anchor] = true; + + // Save nodes in an array so we can create anchor node under it later + headingsArray[anchor] = heading; + } + let menuItems = []; + if (anchorList.length) { + // case insensitive sort + anchorList.sort((a, b) => { + if (a.sortkey < b.sortkey) { + return -1; + } + if (a.sortkey > b.sortkey) { + return 1; + } + return 0; + }); + for (i = 0; i < anchorList.length; i++) { + menuItems.push(createMenuItem(anchorList[i].anchor)); + } + } else { + // Don't bother with named anchors in Mail. + if (editor && editor.flags & Ci.nsIEditor.eEditorMailMask) { + linkMenulist.removeAttribute("enablehistory"); + return; + } + let item = createMenuItem(GetString("NoNamedAnchorsOrHeadings")); + item.setAttribute("disabled", "true"); + menuItems.push(item); + } + window.addEventListener("contextmenu", event => { + if (document.getElementById("datalist-menuseparator")) { + return; + } + let menuseparator = document.createXULElement("menuseparator"); + menuseparator.setAttribute("id", "datalist-menuseparator"); + document.getElementById("textbox-contextmenu").appendChild(menuseparator); + for (let menuitem of menuItems) { + document.getElementById("textbox-contextmenu").appendChild(menuitem); + } + }); + } catch (e) {} +} + +function createMenuItem(label) { + var menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", label); + menuitem.addEventListener("click", event => { + gDialog.hrefInput.value = label; + ChangeLinkLocation(); + }); + return menuitem; +} + +// Shared by Image and Link dialogs for the "Choose" button for links +function chooseLinkFile() { + GetLocalFileURL("html, img").then(fileURL => { + // Always try to relativize local file URLs + if (gHaveDocumentUrl) { + fileURL = MakeRelativeUrl(fileURL); + } + + gDialog.hrefInput.value = fileURL; + + // Do stuff specific to a particular dialog + // (This is defined separately in Image and Link dialogs) + ChangeLinkLocation(); + }); +} |