зеркало из https://github.com/mozilla/pjs.git
1102 строки
31 KiB
JavaScript
1102 строки
31 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla Communicator client code, released
|
|
* March 31, 1998.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Netscape Communications Corporation.
|
|
* Portions created by the Initial Developer are Copyright (C) 1998-1999
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Pete Collins
|
|
* Brian King
|
|
* Charles Manske (cmanske@netscape.com)
|
|
* Neil Rashbrook (neil@parkwaycc.co.uk)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either of the GNU General Public License Version 2 or later (the "GPL"),
|
|
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
// Each editor window must include this file
|
|
|
|
// Object to attach commonly-used widgets (all dialogs should use this)
|
|
var gDialog = {};
|
|
|
|
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 && 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)
|
|
{
|
|
AlertWithTitle(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");
|
|
else
|
|
return GetString("PercentOfWindow");
|
|
} catch (e) { return "";}
|
|
}
|
|
|
|
function ClearListbox(listbox)
|
|
{
|
|
if (listbox)
|
|
{
|
|
listbox.clearSelection();
|
|
while (listbox.firstChild)
|
|
listbox.removeChild(listbox.firstChild);
|
|
}
|
|
}
|
|
|
|
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 LimitStringLength(elementID, length)
|
|
{
|
|
var editField = document.getElementById( elementID );
|
|
if ( !editField )
|
|
return;
|
|
|
|
var stringIn = editField.value;
|
|
if (stringIn && stringIn.length > length)
|
|
editField.value = stringIn.slice(0,length);
|
|
}
|
|
|
|
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 (/%/.test(size))
|
|
{
|
|
// Strip out the %
|
|
size = RegExp.leftContext;
|
|
if (percentItem)
|
|
menulist.selectedItem = percentItem;
|
|
}
|
|
else
|
|
{
|
|
if (/px/.test(size))
|
|
// Strip out the px
|
|
size = RegExp.leftContext;
|
|
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.xul", "_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 = Components.interfaces.nsIFilePicker;
|
|
|
|
function GetLocalFileURL(filterType)
|
|
{
|
|
var fp = Components.classes["@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";
|
|
}
|
|
// Current usage of this is in Link dialog,
|
|
// where we always want HTML first
|
|
else if (filterType.indexOf("html") == 0)
|
|
{
|
|
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.indexOf("img") > 0)
|
|
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);
|
|
|
|
|
|
/* doesn't handle *.shtml files */
|
|
try {
|
|
var ret = fp.show();
|
|
if (ret == nsIFilePicker.returnCancel)
|
|
return null;
|
|
}
|
|
catch (ex) {
|
|
dump("filePicker.chooseInputFile threw an exception\n");
|
|
return null;
|
|
}
|
|
SaveFilePickerDirectory(fp, fileType);
|
|
|
|
var fileHandler = GetFileProtocolHandler();
|
|
return fp.file ? fileHandler.getURLSpecFromFile(fp.file) : null;
|
|
}
|
|
|
|
function GetMetaElement(name)
|
|
{
|
|
if (name)
|
|
{
|
|
name = name.toLowerCase();
|
|
if (name != "")
|
|
{
|
|
var editor = GetCurrentEditor();
|
|
try {
|
|
var metaNodes = editor.document.getElementsByTagName("meta");
|
|
for (var i = 0; i < metaNodes.length; i++)
|
|
{
|
|
var metaNode = metaNodes.item(i);
|
|
if (metaNode && metaNode.getAttribute("name") == name)
|
|
return metaNode;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function CreateMetaElement(name)
|
|
{
|
|
var editor = GetCurrentEditor();
|
|
try {
|
|
var metaElement = editor.createElementWithDefaults("meta");
|
|
metaElement.setAttribute("name", name);
|
|
return metaElement;
|
|
} catch (e) {}
|
|
|
|
return null;
|
|
}
|
|
|
|
function GetHTTPEquivMetaElement(name)
|
|
{
|
|
if (name)
|
|
{
|
|
name = name.toLowerCase();
|
|
if (name != "")
|
|
{
|
|
var editor = GetCurrentEditor();
|
|
try {
|
|
var metaNodes = editor.document.getElementsByTagName("meta");
|
|
for (var i = 0; i < metaNodes.length; i++)
|
|
{
|
|
var metaNode = metaNodes.item(i);
|
|
if (metaNode)
|
|
{
|
|
var httpEquiv = metaNode.getAttribute("http-equiv");
|
|
if (httpEquiv && httpEquiv.toLowerCase() == name)
|
|
return metaNode;
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function CreateHTTPEquivMetaElement(name)
|
|
{
|
|
var editor = GetCurrentEditor();
|
|
try {
|
|
var metaElement = editor.createElementWithDefaults("meta");
|
|
metaElement.setAttribute("http-equiv", name);
|
|
return metaElement;
|
|
} catch (e) {}
|
|
|
|
return null;
|
|
}
|
|
|
|
function CreateHTTPEquivElement(name)
|
|
{
|
|
var editor = GetCurrentEditor();
|
|
try {
|
|
var metaElement = editor.createElementWithDefaults("meta");
|
|
metaElement.setAttribute("http-equiv", name);
|
|
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 {
|
|
var headList = editor.document.getElementsByTagName("head");
|
|
return headList.item(0);
|
|
} catch (e) {}
|
|
|
|
return null;
|
|
}
|
|
|
|
function PrependHeadElement(element)
|
|
{
|
|
var head = GetHeadElement();
|
|
if (head)
|
|
{
|
|
var editor = GetCurrentEditor();
|
|
try {
|
|
// Use editor's undoable transaction
|
|
// Last param "true" says "don't change the selection"
|
|
editor.insertNode(element, head, 0, true);
|
|
} 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
|
|
// Last param "true" says "don't change the selection"
|
|
editor.insertNode(element, head, position, true);
|
|
} 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)
|
|
{
|
|
var newOffsetX = window.screenX - window.opener.screenX;
|
|
var newOffsetY = window.screenY - window.opener.screenY;
|
|
gLocation.setAttribute("offsetX", window.screenX - window.opener.screenX);
|
|
gLocation.setAttribute("offsetY", window.screenY - window.opener.screenY);
|
|
}
|
|
}
|
|
|
|
function onCancel()
|
|
{
|
|
SaveWindowLocation();
|
|
// Close dialog by returning true
|
|
return true;
|
|
}
|
|
|
|
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 & Components.interfaces.nsIPlaintextEditor.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
|
|
{
|
|
// 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)
|
|
if (url[0] == "#")
|
|
{
|
|
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.xul
|
|
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
|
|
AlertWithTitle("", 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: true,
|
|
BLOCKQUOTE: true,
|
|
BODY: true,
|
|
CENTER: true,
|
|
DD: true,
|
|
DIV: true,
|
|
FORM: true,
|
|
LI: true,
|
|
NOSCRIPT: true,
|
|
OBJECT: true,
|
|
TD: true,
|
|
TH: true
|
|
};
|
|
|
|
var NotAnInlineParent = {
|
|
COL: true,
|
|
COLGROUP: true,
|
|
DL: true,
|
|
DIR: true,
|
|
MENU: true,
|
|
OL: true,
|
|
TABLE: true,
|
|
TBODY: true,
|
|
TFOOT: true,
|
|
THEAD: true,
|
|
TR: true,
|
|
UL: true
|
|
};
|
|
|
|
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 (!(range.commonAncestorContainer.localName in IsBlockParent))
|
|
range.selectNode(range.commonAncestorContainer);
|
|
else
|
|
{
|
|
// Fail if we're not inserting a block (use setInlineProperty instead)
|
|
if (!nodeIsBreak(editor, range.commonAncestorContainer))
|
|
return false;
|
|
else if (range.commonAncestorContainer.localName in NotAnInlineParent)
|
|
// Inline element parent must not be an invalid block
|
|
do range.selectNode(range.commonAncestorContainer);
|
|
while (range.commonAncestorContainer.localName in NotAnInlineParent);
|
|
else
|
|
// Further insert block check
|
|
for (var i = range.startOffset; ; i++)
|
|
if (i == range.endOffset)
|
|
return false;
|
|
else 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
|
|
editor.insertNode(element, range.commonAncestorContainer, offset, true);
|
|
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: anchor, sortkey: anchor.toLowerCase()});
|
|
anchorMap[anchor] = true;
|
|
}
|
|
}
|
|
|
|
// grab IDs
|
|
if (element.id)
|
|
{
|
|
anchor = '#' + element.id;
|
|
if (!(anchor in anchorMap))
|
|
{
|
|
anchorList.push({anchor: 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: 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
|
|
function compare(a, b)
|
|
{
|
|
if(a.sortkey < b.sortkey) return -1;
|
|
if(a.sortkey > b.sortkey) return 1;
|
|
return 0;
|
|
}
|
|
anchorList.sort(compare);
|
|
|
|
for (i = 0; i < anchorList.length; i++)
|
|
createMenuItem(menupopup,anchorList[i].anchor);
|
|
}
|
|
else
|
|
{
|
|
var item = createMenuItem(menupopup, GetString("NoNamedAnchorsOrHeadings"));
|
|
item.setAttribute("disabled", "true");
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function createMenuItem(aMenuPopup, aLabel)
|
|
{
|
|
var menuitem = document.createElement("menuitem");
|
|
menuitem.setAttribute("label", aLabel);
|
|
aMenuPopup.appendChild(menuitem);
|
|
return menuitem;
|
|
}
|
|
|
|
// Shared by Image and Link dialogs for the "Choose" button for links
|
|
function chooseLinkFile()
|
|
{
|
|
// Get a local file, converted into URL format
|
|
var fileName = GetLocalFileURL("html, img");
|
|
if (fileName)
|
|
{
|
|
// Always try to relativize local file URLs
|
|
if (gHaveDocumentUrl)
|
|
fileName = MakeRelativeUrl(fileName);
|
|
|
|
gDialog.hrefInput.value = fileName;
|
|
|
|
// Do stuff specific to a particular dialog
|
|
// (This is defined separately in Image and Link dialogs)
|
|
ChangeLinkLocation();
|
|
}
|
|
// Put focus into the input field
|
|
SetTextboxFocus(gDialog.hrefInput);
|
|
}
|
|
|