diff options
Diffstat (limited to 'comm/suite/editor/components/dialogs/content/EdDialogCommon.js')
-rw-r--r-- | comm/suite/editor/components/dialogs/content/EdDialogCommon.js | 1038 |
1 files changed, 1038 insertions, 0 deletions
diff --git a/comm/suite/editor/components/dialogs/content/EdDialogCommon.js b/comm/suite/editor/components/dialogs/content/EdDialogCommon.js new file mode 100644 index 0000000000..c6b7c63778 --- /dev/null +++ b/comm/suite/editor/components/dialogs/content/EdDialogCommon.js @@ -0,0 +1,1038 @@ +/* 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 ../../composer/content/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 + +// For dialogs that expand in size. Default is smaller size see "onMoreFewer()" below +var SeeMore = false; + +// 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 'textbox' XUL element for text input of 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, + mustShowMoreSection +) { + 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(); + + // or expand dialog for users of "More / Fewer" button + if ( + "dialog" in window && + window.dialog && + "MoreSection" in gDialog && + gDialog.MoreSection + ) { + if (!SeeMore) { + onMoreFewer(); + } + } + + // 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(textbox) { + if (textbox) { + // XXX Using the setTimeout is hacky workaround for bug 103197 + // Must create a new function to keep "textbox" in scope + setTimeout( + function(textbox) { + textbox.focus(); + textbox.select(); + }, + 0, + textbox + ); + } +} + +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://editor/content/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 getColorAndSetColorWell(ColorPickerID, ColorWellID) { + var color = getColor(ColorPickerID); + setColorWell(ColorWellID, color); + return color; +} + +function InitMoreFewer() { + // Set SeeMore bool to the OPPOSITE of the current state, + // which is automatically saved by using the 'persist="more"' + // attribute on the gDialog.MoreFewerButton button + // onMoreFewer will toggle it and redraw the dialog + SeeMore = gDialog.MoreFewerButton.getAttribute("more") != "1"; + onMoreFewer(); + gDialog.MoreFewerButton.setAttribute( + "accesskey", + GetString("PropertiesAccessKey") + ); +} + +function onMoreFewer() { + if (SeeMore) { + gDialog.MoreSection.collapsed = true; + gDialog.MoreFewerButton.setAttribute("more", "0"); + gDialog.MoreFewerButton.setAttribute("label", GetString("MoreProperties")); + SeeMore = false; + } else { + gDialog.MoreSection.collapsed = false; + gDialog.MoreFewerButton.setAttribute("more", "1"); + gDialog.MoreFewerButton.setAttribute("label", GetString("FewerProperties")); + SeeMore = true; + } + window.sizeToContent(); +} + +function SwitchToValidatePanel() { + // no default implementation + // Only EdTableProps.js currently implements this +} + +const nsIFilePicker = Ci.nsIFilePicker; + +/** + * @return {Promise} URL spec of the file chosen, or null + */ +function GetLocalFileURL(filterType) { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + var fileType = "html"; + + if (filterType == "img") { + fp.init(window, GetString("SelectImageFile"), nsIFilePicker.modeOpen); + fp.appendFilters(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"), 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(nsIFilePicker.filterHTML); + fp.appendFilters(nsIFilePicker.filterText); + + // Link dialog also allows linking to images + if (filterType.includes("img", 1)) { + fp.appendFilters(nsIFilePicker.filterImages); + } + } + // Default or last filter is "All Files" + fp.appendFilters(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 != nsIFilePicker.returnOK || !fp.file) { + resolve(null); + return; + } + SaveFilePickerDirectory(fp, fileType); + resolve(fp.fileURL.spec); + }); + }); +} + +function GetMetaElementByAttribute(name, value) { + if (name) { + name = name.toLowerCase(); + let editor = GetCurrentEditor(); + try { + return editor.document.querySelector( + "meta[" + name + '="' + value + '"]' + ); + } catch (e) {} + } + return null; +} + +function CreateMetaElementWithAttribute(name, value) { + let editor = GetCurrentEditor(); + try { + let metaElement = editor.createElementWithDefaults("meta"); + if (name) { + metaElement.setAttribute(name, value); + } + return metaElement; + } catch (e) {} + return null; +} + +// Change "content" attribute on a META element, +// or delete entire element it if content is empty +// This uses undoable editor transactions +function SetMetaElementContent(metaElement, content, insertNew, prepend) { + if (metaElement) { + var editor = GetCurrentEditor(); + try { + if (!content || content == "") { + if (!insertNew) { + editor.deleteNode(metaElement); + } + } else if (insertNew) { + metaElement.setAttribute("content", content); + if (prepend) { + PrependHeadElement(metaElement); + } else { + AppendHeadElement(metaElement); + } + } else { + editor.setAttribute(metaElement, "content", content); + } + } catch (e) {} + } +} + +function GetHeadElement() { + var editor = GetCurrentEditor(); + try { + return editor.document.querySelector("head"); + } catch (e) {} + + return null; +} + +function PrependHeadElement(element) { + var head = GetHeadElement(); + if (head) { + var editor = GetCurrentEditor(); + try { + // Use editor's undoable transaction + // XXX Here tried to prevent updating Selection with unknown 4th argument, + // but nsIEditor.setShouldTxnSetSelection is not used for that. + editor.insertNode(element, head, 0); + } catch (e) {} + } +} + +function AppendHeadElement(element) { + var head = GetHeadElement(); + if (head) { + var position = 0; + if (head.hasChildNodes()) { + position = head.childNodes.length; + } + + var editor = GetCurrentEditor(); + try { + // Use editor's undoable transaction + // XXX Here tried to prevent updating Selection with unknown 4th argument, + // but nsIEditor.setShouldTxnSetSelection is not used for that. + editor.insertNode(element, head, position); + } catch (e) {} + } +} + +function SetWindowLocation() { + gLocation = document.getElementById("location"); + if (gLocation) { + window.screenX = Math.max( + 0, + Math.min( + window.opener.screenX + Number(gLocation.getAttribute("offsetX")), + screen.availWidth - window.outerWidth + ) + ); + window.screenY = Math.max( + 0, + Math.min( + window.opener.screenY + Number(gLocation.getAttribute("offsetY")), + screen.availHeight - window.outerHeight + ) + ); + } +} + +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 nodeIsBreak(editor, node) { + return !node || node.localName == "br" || editor.nodeIsBlock(node); +} + +function InsertElementAroundSelection(element) { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + + try { + // First get the selection as a single range + var range, start, end, offset; + var count = editor.selection.rangeCount; + if (count == 1) { + range = editor.selection.getRangeAt(0).cloneRange(); + } else { + range = editor.document.createRange(); + start = editor.selection.getRangeAt(0); + range.setStart(start.startContainer, start.startOffset); + end = editor.selection.getRangeAt(--count); + range.setEnd(end.endContainer, end.endOffset); + } + + // Flatten the selection to child nodes of the common ancestor + while (range.startContainer != range.commonAncestorContainer) { + range.setStartBefore(range.startContainer); + } + while (range.endContainer != range.commonAncestorContainer) { + range.setEndAfter(range.endContainer); + } + + if (editor.nodeIsBlock(element)) { + // Block element parent must be a valid block + while (!IsBlockParent.includes(range.commonAncestorContainer.localName)) { + range.selectNode(range.commonAncestorContainer); + } + } else { + if (!nodeIsBreak(editor, range.commonAncestorContainer)) { + // Fail if we're not inserting a block (use setInlineProperty instead) + return false; + } + if (NotAnInlineParent.includes(range.commonAncestorContainer.localName)) { + // Inline element parent must not be an invalid block + do { + range.selectNode(range.commonAncestorContainer); + } while ( + NotAnInlineParent.includes(range.commonAncestorContainer.localName) + ); + } else { + // Further insert block check + for (var i = range.startOffset; ; i++) { + if (i == range.endOffset) { + return false; + } + if ( + nodeIsBreak(editor, range.commonAncestorContainer.childNodes[i]) + ) { + break; + } + } + } + } + + // The range may be contained by body text, which should all be selected. + offset = range.startOffset; + start = range.startContainer.childNodes[offset]; + if (!nodeIsBreak(editor, start)) { + while (!nodeIsBreak(editor, start.previousSibling)) { + start = start.previousSibling; + offset--; + } + } + end = range.endContainer.childNodes[range.endOffset]; + if (end && !nodeIsBreak(editor, end.previousSibling)) { + while (!nodeIsBreak(editor, end)) { + end = end.nextSibling; + } + } + + // Now insert the node + // XXX Here tried to prevent updating Selection with unknown 4th argument, + // but nsIEditor.setShouldTxnSetSelection is not used for that. + editor.insertNode(element, range.commonAncestorContainer, offset); + offset = element.childNodes.length; + if (!editor.nodeIsBlock(element)) { + editor.setShouldTxnSetSelection(false); + } + + // Move all the old child nodes to the element + var empty = true; + while (start != end) { + var next = start.nextSibling; + editor.deleteNode(start); + editor.insertNode(start, element, element.childNodes.length); + empty = false; + start = next; + } + if (!editor.nodeIsBlock(element)) { + editor.setShouldTxnSetSelection(true); + } else { + // Also move a trailing <br> + if (start && start.localName == "br") { + editor.deleteNode(start); + editor.insertNode(start, element, element.childNodes.length); + empty = false; + } + // Still nothing? Insert a <br> so the node is not empty + if (empty) { + editor.insertNode( + editor.createElementWithDefaults("br"), + element, + element.childNodes.length + ); + } + + // Hack to set the selection just inside the element + editor.insertNode(editor.document.createTextNode(""), element, offset); + } + } finally { + editor.endTransaction(); + } + + return true; +} + +function nodeIsBlank(node) { + return node && node.nodeType == Node.TEXT_NODE && !/\S/.test(node.data); +} + +function nodeBeginsBlock(editor, node) { + while (nodeIsBlank(node)) { + node = node.nextSibling; + } + return nodeIsBreak(editor, node); +} + +function nodeEndsBlock(editor, node) { + while (nodeIsBlank(node)) { + node = node.previousSibling; + } + return nodeIsBreak(editor, node); +} + +// C++ function isn't exposed to JS :-( +function RemoveBlockContainer(element) { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + + try { + var range = editor.document.createRange(); + range.selectNode(element); + var offset = range.startOffset; + var parent = element.parentNode; + + // May need to insert a break after the removed element + if ( + !nodeBeginsBlock(editor, element.nextSibling) && + !nodeEndsBlock(editor, element.lastChild) + ) { + editor.insertNode( + editor.createElementWithDefaults("br"), + parent, + range.endOffset + ); + } + + // May need to insert a break before the removed element, or if it was empty + if ( + !nodeEndsBlock(editor, element.previousSibling) && + !nodeBeginsBlock(editor, element.firstChild || element.nextSibling) + ) { + editor.insertNode( + editor.createElementWithDefaults("br"), + parent, + offset++ + ); + } + + // Now remove the element + editor.deleteNode(element); + + // Need to copy the contained nodes? + for (var i = 0; i < element.childNodes.length; i++) { + editor.insertNode( + element.childNodes[i].cloneNode(true), + parent, + offset++ + ); + } + } finally { + editor.endTransaction(); + } +} + +// C++ function isn't exposed to JS :-( +function RemoveContainer(element) { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + + try { + var range = editor.document.createRange(); + var parent = element.parentNode; + // Allow for automatic joining of text nodes + // so we can't delete the container yet + // so we need to copy the contained nodes + for (var i = 0; i < element.childNodes.length; i++) { + range.selectNode(element); + editor.insertNode( + element.childNodes[i].cloneNode(true), + parent, + range.startOffset + ); + } + // Now remove the element + editor.deleteNode(element); + } finally { + editor.endTransaction(); + } +} + +function FillLinkMenulist(linkMenulist, headingsArray) { + var menupopup = linkMenulist.firstChild; + 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 ( + element instanceof HTMLHeadingElement && + element.textContent && + !( + element.firstChild instanceof HTMLAnchorElement && + element.firstChild.name + ) + ) { + headingList.push(element); + } + + // grab named anchors + if (element instanceof HTMLAnchorElement && 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; + } + 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++) { + createMenuItem(menupopup, anchorList[i].anchor); + } + } else { + // Don't bother with named anchors in Mail. + if (editor && editor.flags & Ci.nsIEditor.eEditorMailMask) { + menupopup.remove(); + linkMenulist.removeAttribute("enablehistory"); + return; + } + var item = createMenuItem( + menupopup, + GetString("NoNamedAnchorsOrHeadings") + ); + item.setAttribute("disabled", "true"); + } + } catch (e) {} +} + +function createMenuItem(aMenuPopup, aLabel) { + var menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", aLabel); + aMenuPopup.appendChild(menuitem); + 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(); + }); +} |