/*
 * The contents of this file are subject to the Netscape 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/NPL/
 *
 * 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 Netscape are
 * Copyright (C) 1998-1999 Netscape Communications Corporation. All
 * Rights Reserved.
 *
 * Contributor(s):
 *   Pete Collins
 *   Brian King
 *   Charles Manske (cmanske@netscape.com)
 *   Neil Rashbrook (neil@parkwaycc.co.uk)
 */

// 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)
        element.setAttribute(attName, numString);
    }
  } else if (element) {
    element.removeAttribute(attName);
  }
  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 refering 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();
}

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 nodeDepth(node)
{
  for (var depth = 0; node != null; depth++)
    node = node.parentNode;
  return depth;
}

function nthParent(node, n)
{
  for (; n > 0; n--)
    node = node.parentNode;
  return node;
}

function nodeIsBlock(node)
{
  // HR doesn't count because it's not a container
  var editor = GetCurrentEditor();
  return !editor || !node || 
          (node.localName != 'HR' && editor.nodeIsBlock(node));
}

/* Ugly code alert! If only I could do this:
 * var range = editor.selection.flattenRange(range); // ensure anchorNode == parentNode
 * if (editor.isInlineRange(range) && editor.nodeIsBlock(node)) return false;
 * while (!editor.containmentAllowed(range.anchorNode, element))
 *   range = editor.parentRangeOf(range);
 * editor.insertNodeAtRange(element, range);
 * return true;
 */
function InsertElementAroundSelection(element)
{
  var editor = GetCurrentEditor();

  try {
    // We need to find a suitable container for the element.
    // First get the selection
    var anchorParent = editor.selection.anchorNode;
    if (!anchorParent.localName)
      var anchorSelected = true;
    else if (editor.selection.anchorOffset < anchorParent.childNodes.length)
      var anchor = anchorParent.childNodes[editor.selection.anchorOffset];
    var focusParent = editor.selection.focusNode;
    if (!focusParent.localName)
      var focusSelected = true;
    else if (editor.selection.focusOffset < focusParent.childNodes.length)
      var focus = focusParent.childNodes[editor.selection.focusOffset];

    // Find the common ancestor
    var anchorDepth = nodeDepth(anchorParent);
    var focusDepth = nodeDepth(focusParent);
    if (anchorDepth > focusDepth)
    {
      anchor = nthParent(anchorParent, anchorDepth - focusDepth - 1);
      anchorParent = anchor.parentNode;
      anchorSelected = true;
    }
    else if (anchorDepth < focusDepth)
    {
      focus = nthParent(focusParent, focusDepth - anchorDepth - 1);
      focusParent = focus.parentNode;
      focusSelected = true;
    }
    var ordered = false;
    while (anchorParent != focusParent)
    {
      anchor = anchorParent;
      anchorParent = anchor.parentNode;
      focus = focusParent;
      focusParent = focus.parentNode;
      anchorSelected = focusSelected = true;
    }

    // The common ancestor may not be suitable, so find a suitable one.
    if (editor.nodeIsBlock(element))
    {
      // Block element parent must be a valid block
      while (!(anchorParent.localName in IsBlockParent))
      {
        anchor = focus = anchorParent;
        anchorSelected = focusSelected = true;
        anchorParent = anchor.parentNode;
        ordered = true;
      }
    }
    else
    {
      // Inline element parent must not be an invalid block
      while (anchorParent.localName in NotAnInlineParent)
      {
        anchor = focus = anchorParent;
        anchorSelected = focusSelected = true;
        anchorParent = anchor.parentNode;
        ordered = true;
      }
    }

    // We now have an ancestor to hold the element
    // and a range of child nodes to move into the element
    if (anchor != focus)
    {
      if (!ordered)
      {
        // Ensure anchor <= focus
        for (var node = anchorParent.firstChild; node != anchor; node = node.nextSibling)
        {
          if (node == focus)
          {
            focus = anchor;
            anchor = node;
            focusSelected = anchorSelected;
            break;
          }
        }
      }
      if (focus && !focusSelected)
        focus = focus.previousSibling;
    }
    if (!editor.nodeIsBlock(element))
    {
      // Fail if we're not inserting a block
      if (!anchor) return false;
      for (node = anchor; ; node = node.nextSibling)
        if (!node)
          return false;
        else if (nodeIsBlock(node))
          break;
        else if (node == focus)
          return false;
    }

    // The range may be contained by body text, which should all be selected.
    if (!nodeIsBlock(anchor))
      while (!nodeIsBlock(anchor.previousSibling))
        anchor = anchor.previousSibling;
    if (!nodeIsBlock(focus))
      while (!nodeIsBlock(focus.nextSibling))
        focus = focus.nextSibling;
  } catch (e) {return false;}

  editor.beginTransaction();
  try {
    var anchorOffset = 0;
    // Calculate the insertion point for the undoable insertNode method
    if (!anchor)
      anchor = anchorParent.firstChild;
    else
      for (node = anchorParent.firstChild; node != anchor; node = node.nextSibling)
        anchorOffset++;
    editor.insertNode(element, anchorParent, anchorOffset, true);
    // Move all the old child nodes to the element
    // Use editor methods in case of text nodes
    while (anchor)
    {
      node = anchor.nextSibling;
      editor.deleteNode(anchor);
      editor.insertNode(anchor, element, element.childNodes.length);
      if (anchor == focus) break;
      anchor = node;
    }
  }
  catch (ex) {}

  editor.endTransaction();
  return true;
}

// Should I set the selection to the element, then insert HTML the element's innerHTML?
// I would prefer to say editor.deleteNode(element, FLAG_TO_KEEP_CHILD_NODES);
function RemoveElementKeepingChildren(element)
{
  var editor = GetCurrentEditor();

  try {
    editor.beginTransaction();
    if (element.firstChild)
    {
      // Use editor methods in case of text nodes
      var parent = element.parentNode;
      var offset = 0;
      for (var node = parent.firstChild; node != element; node = node.nextSibling)
        offset++;
      while ((node = element.firstChild))
      {
        editor.deleteNode(node);
        editor.insertNode(node, parent, offset++);
      }
    }
    editor.deleteNode(element);
  }
  catch (ex) {}

  editor.endTransaction();
}

function FillLinkMenulist(linkMenulist, headingsArray)
{
  var editor = GetCurrentEditor();
  try {
    var NamedAnchorNodeList = editor.document.anchors;
    var NamedAnchorCount = NamedAnchorNodeList.length;
    if (NamedAnchorCount > 0)
    {
      for (var i = 0; i < NamedAnchorCount; i++)
        linkMenulist.appendItem("#" + NamedAnchorNodeList.item(i).name);
    } 
    for (var j = 1; j <= 6; j++)
    {
      var headingList = editor.document.getElementsByTagName("h" + j);
      for (var k = 0; k < headingList.length; k++)
      {
        var heading = headingList.item(k);

        // 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)
        var child = heading.firstChild;
        if (child && child.nodeName == "A" && child.name && (child.name.length>0))
          continue;

        var range = editor.document.createRange();
        range.setStart(heading,0);
        var lastChildIndex = heading.childNodes.length;
        range.setEnd(heading,lastChildIndex);
        var text = range.toString();
        if (text)
        {
          // Use just first 40 characters, don't add "...",
          //  and replace whitespace with "_" and strip non-word characters
          text = "#" + ConvertToCDATAString(TruncateStringAtWordEnd(text, 40, false));
          // Append "_" to any name already in the list
          while (linkMenulist.getElementsByAttribute("label", text).length)
            text += "_";
          linkMenulist.appendItem(text);

          // Save nodes in an array so we can create anchor node under it later
          headingsArray[text] = heading;
        }
      }
    }
    if (!linkMenulist.firstChild.hasChildNodes()) 
    {
      var item = linkMenulist.appendItem(GetString("NoNamedAnchorsOrHeadings"));
      item.setAttribute("disabled", "true");
    }
  } catch (e) {}
}

// 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);
}

