Revision control

Copy as Markdown

Other Tools

/* 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(
"_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
// before bug 1764895, but nsIEditor.setShouldTxnSetSelection was 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
// before bug 1764895, but nsIEditor.setShouldTxnSetSelection was 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
// before bug 1764895, but nsIEditor.setShouldTxnSetSelection was not
// used for that.
editor.insertNode(element, range.commonAncestorContainer, offset);
offset = element.childNodes.length;
// Move all the old child nodes to the element
const preserveSelection = !editor.nodeIsBlock(element);
var empty = true;
while (start != end) {
var next = start.nextSibling;
editor.deleteNode(start, preserveSelection);
editor.insertNode(start, element, element.childNodes.length, preserveSelection);
empty = false;
start = next;
}
// FYI: nsIHTMLEditor.nodeIsBlock may return different value if it's moved
// or removed from the old position or the style has been changed.
// Therefore, the result may be different from `!preserveSelection`.
if (editor.nodeIsBlock(element)) {
// 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();
});
}