home *** CD-ROM | disk | FTP | other *** search
/ Chip 2004 April / CMCD0404.ISO / Software / Complet / thunderbird / chrome / mail.jar / content / editor / EdDialogCommon.js < prev    next >
Encoding:
JavaScript  |  2004-01-11  |  30.6 KB  |  1,058 lines

  1. /*
  2.  * The contents of this file are subject to the Netscape Public
  3.  * License Version 1.1 (the "License"); you may not use this file
  4.  * except in compliance with the License. You may obtain a copy of
  5.  * the License at http://www.mozilla.org/NPL/
  6.  *
  7.  * Software distributed under the License is distributed on an "AS
  8.  * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  9.  * implied. See the License for the specific language governing
  10.  * rights and limitations under the License.
  11.  *
  12.  * The Original Code is Mozilla Communicator client code, released
  13.  * March 31, 1998.
  14.  *
  15.  * The Initial Developer of the Original Code is Netscape
  16.  * Communications Corporation. Portions created by Netscape are
  17.  * Copyright (C) 1998-1999 Netscape Communications Corporation. All
  18.  * Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *   Pete Collins
  22.  *   Brian King
  23.  *   Charles Manske (cmanske@netscape.com)
  24.  *   Neil Rashbrook (neil@parkwaycc.co.uk)
  25.  */
  26.  
  27. // Each editor window must include this file
  28.  
  29. // Object to attach commonly-used widgets (all dialogs should use this)
  30. var gDialog = {};
  31.  
  32. var gValidationError = false;
  33.  
  34. // Use for 'defaultIndex' param in InitPixelOrPercentMenulist
  35. const gPixel = 0;
  36. const gPercent = 1;
  37.  
  38. const gMaxPixels  = 100000; // Used for image size, borders, spacing, and padding
  39. // Gecko code uses 1000 for maximum rowspan, colspan
  40. // Also, editing performance is really bad above this
  41. const gMaxRows    = 1000;
  42. const gMaxColumns = 1000;
  43. const gMaxTableSize = 1000000; // Width or height of table or cells
  44.  
  45. // For dialogs that expand in size. Default is smaller size see "onMoreFewer()" below
  46. var SeeMore = false;
  47.  
  48. // A XUL element with id="location" for managing
  49. // dialog location relative to parent window
  50. var gLocation;
  51.  
  52. // The element being edited - so AdvancedEdit can have access to it
  53. var globalElement;
  54.  
  55. /* Validate contents of an input field 
  56.  *
  57.  *  inputWidget    The 'textbox' XUL element for text input of the attribute's value
  58.  *  listWidget     The 'menulist' XUL element for choosing "pixel" or "percent"
  59.  *                  May be null when no pixel/percent is used.
  60.  *  minVal         minimum allowed for input widget's value
  61.  *  maxVal         maximum allowed for input widget's value
  62.  *                 (when "listWidget" is used, maxVal is used for "pixel" maximum,
  63.  *                  100% is assumed if "percent" is the user's choice)
  64.  *  element        The DOM element that we set the attribute on. May be null.
  65.  *  attName        Name of the attribute to set.  May be null or ignored if "element" is null
  66.  *  mustHaveValue  If true, error dialog is displayed if "value" is empty string
  67.  *
  68.  *  This calls "ValidateNumberRange()", which puts up an error dialog to inform the user. 
  69.  *    If error, we also: 
  70.  *      Shift focus and select contents of the inputWidget,
  71.  *      Switch to appropriate panel of tabbed dialog if user implements "SwitchToValidate()",
  72.  *      and/or will expand the dialog to full size if "More / Fewer" feature is implemented
  73.  *
  74.  *  Returns the "value" as a string, or "" if error or input contents are empty
  75.  *  The global "gValidationError" variable is set true if error was found
  76.  */
  77. function ValidateNumber(inputWidget, listWidget, minVal, maxVal, element, attName, mustHaveValue, mustShowMoreSection)
  78. {
  79.   if (!inputWidget)
  80.   {
  81.     gValidationError = true;
  82.     return "";
  83.   }
  84.  
  85.   // Global error return value
  86.   gValidationError = false;
  87.   var maxLimit = maxVal;
  88.   var isPercent = false;
  89.  
  90.   var numString = TrimString(inputWidget.value);
  91.   if (numString || mustHaveValue)
  92.   {
  93.     if (listWidget)
  94.       isPercent = (listWidget.selectedIndex == 1);
  95.     if (isPercent)
  96.       maxLimit = 100;
  97.  
  98.     // This method puts up the error message
  99.     numString = ValidateNumberRange(numString, minVal, maxLimit, mustHaveValue);
  100.     if(!numString)
  101.     {
  102.       // Switch to appropriate panel for error reporting
  103.       SwitchToValidatePanel();
  104.  
  105.       // or expand dialog for users of "More / Fewer" button
  106.       if ("dialog" in window && dialog && 
  107.            "MoreSection" in gDialog && gDialog.MoreSection)
  108.       {
  109.         if ( !SeeMore )
  110.           onMoreFewer();
  111.       }
  112.  
  113.       // Error - shift to offending input widget
  114.       SetTextboxFocus(inputWidget);
  115.       gValidationError = true;
  116.     }
  117.     else
  118.     {
  119.       if (isPercent)
  120.         numString += "%";
  121.       if (element)
  122.         element.setAttribute(attName, numString);
  123.     }
  124.   } else if (element) {
  125.     GetCurrentEditor().removeAttributeOrEquivalent(element, attName, true)
  126.   }
  127.   return numString;
  128. }
  129.  
  130. /* Validate contents of an input field 
  131.  *
  132.  *  value          number to validate
  133.  *  minVal         minimum allowed for input widget's value
  134.  *  maxVal         maximum allowed for input widget's value
  135.  *                 (when "listWidget" is used, maxVal is used for "pixel" maximum,
  136.  *                  100% is assumed if "percent" is the user's choice)
  137.  *  mustHaveValue  If true, error dialog is displayed if "value" is empty string
  138.  *
  139.  *  If inputWidget's value is outside of range, or is empty when "mustHaveValue" = true,
  140.  *      an error dialog is popuped up to inform the user. The focus is shifted
  141.  *      to the inputWidget.
  142.  *
  143.  *  Returns the "value" as a string, or "" if error or input contents are empty
  144.  *  The global "gValidationError" variable is set true if error was found
  145.  */
  146. function ValidateNumberRange(value, minValue, maxValue, mustHaveValue)
  147. {
  148.   // Initialize global error flag
  149.   gValidationError = false;
  150.   value = TrimString(String(value));
  151.  
  152.   // We don't show error for empty string unless caller wants to
  153.   if (!value && !mustHaveValue)
  154.     return "";
  155.  
  156.   var numberStr = "";
  157.  
  158.   if (value.length > 0)
  159.   {
  160.     // Extract just numeric characters
  161.     var number = Number(value.replace(/\D+/g, ""));
  162.     if (number >= minValue && number <= maxValue )
  163.     {
  164.       // Return string version of the number
  165.       return String(number);
  166.     }
  167.     numberStr = String(number);
  168.   }
  169.  
  170.   var message = "";
  171.  
  172.   if (numberStr.length > 0)
  173.   {
  174.     // We have a number from user outside of allowed range
  175.     message = GetString( "ValidateRangeMsg");
  176.     message = message.replace(/%n%/, numberStr);
  177.     message += "\n ";
  178.   }
  179.   message += GetString( "ValidateNumberMsg");
  180.  
  181.   // Replace variable placeholders in message with number values
  182.   message = message.replace(/%min%/, minValue).replace(/%max%/, maxValue);
  183.   ShowInputErrorMessage(message);
  184.  
  185.   // Return an empty string to indicate error
  186.   gValidationError = true;
  187.   return "";
  188. }
  189.  
  190. function SetTextboxFocusById(id)
  191. {
  192.   SetTextboxFocus(document.getElementById(id));
  193. }
  194.  
  195. function SetTextboxFocus(textbox)
  196. {
  197.   if (textbox)
  198.   {
  199.     //XXX Using the setTimeout is hacky workaround for bug 103197
  200.     // Must create a new function to keep "textbox" in scope
  201.     setTimeout( function(textbox) { textbox.focus(); textbox.select(); }, 0, textbox );
  202.   }
  203. }
  204.  
  205. function ShowInputErrorMessage(message)
  206. {
  207.   AlertWithTitle(GetString("InputError"), message);
  208.   window.focus();
  209. }
  210.  
  211. // Get the text appropriate to parent container
  212. //  to determine what a "%" value is referring to.
  213. // elementForAtt is element we are actually setting attributes on
  214. //  (a temporary copy of element in the doc to allow canceling),
  215. //  but elementInDoc is needed to find parent context in document
  216. function GetAppropriatePercentString(elementForAtt, elementInDoc)
  217. {
  218.   var editor = GetCurrentEditor();
  219.   try {
  220.     var name = elementForAtt.nodeName.toLowerCase();
  221.     if ( name == "td" || name == "th")
  222.       return GetString("PercentOfTable");
  223.  
  224.     // Check if element is within a table cell
  225.     if (editor.getElementOrParentByTagName("td", elementInDoc))
  226.       return GetString("PercentOfCell");
  227.     else
  228.       return GetString("PercentOfWindow");
  229.   } catch (e) { return "";}
  230. }
  231.  
  232. function ClearListbox(listbox)
  233. {
  234.   if (listbox)
  235.   {
  236.     listbox.clearSelection();
  237.     while (listbox.firstChild)
  238.       listbox.removeChild(listbox.firstChild);
  239.   }
  240. }
  241.  
  242. function forceInteger(elementID)
  243. {
  244.   var editField = document.getElementById( elementID );
  245.   if ( !editField )
  246.     return;
  247.  
  248.   var stringIn = editField.value;
  249.   if (stringIn && stringIn.length > 0)
  250.   {
  251.     // Strip out all nonnumeric characters
  252.     stringIn = stringIn.replace(/\D+/g,"");
  253.     if (!stringIn) stringIn = "";
  254.  
  255.     // Write back only if changed
  256.     if (stringIn != editField.value)
  257.       editField.value = stringIn;
  258.   }
  259. }
  260.  
  261. function LimitStringLength(elementID, length)
  262. {
  263.   var editField = document.getElementById( elementID );
  264.   if ( !editField )
  265.     return;
  266.  
  267.   var stringIn = editField.value;
  268.   if (stringIn && stringIn.length > length)
  269.     editField.value = stringIn.slice(0,length);
  270. }
  271.  
  272. function InitPixelOrPercentMenulist(elementForAtt, elementInDoc, attribute, menulistID, defaultIndex)
  273. {
  274.   if (!defaultIndex) defaultIndex = gPixel;
  275.  
  276.   // var size  = elementForAtt.getAttribute(attribute);
  277.   var size = GetHTMLOrCSSStyleValue(elementForAtt, attribute, attribute)
  278.   var menulist = document.getElementById(menulistID);
  279.   var pixelItem;
  280.   var percentItem;
  281.  
  282.   if (!menulist)
  283.   {
  284.     dump("NO MENULIST found for ID="+menulistID+"\n");
  285.     return size;
  286.   }
  287.  
  288.   menulist.removeAllItems();
  289.   pixelItem = menulist.appendItem(GetString("Pixels"));
  290.  
  291.   if (!pixelItem) return 0;
  292.  
  293.   percentItem = menulist.appendItem(GetAppropriatePercentString(elementForAtt, elementInDoc));
  294.   if (size && size.length > 0)
  295.   {
  296.     // Search for a "%" or "px"
  297.     if (/%/.test(size))
  298.     {
  299.       // Strip out the %
  300.       size = RegExp.leftContext;
  301.       if (percentItem)
  302.         menulist.selectedItem = percentItem;
  303.     }
  304.     else
  305.     {
  306.       if (/px/.test(size))
  307.         // Strip out the px
  308.         size = RegExp.leftContext;
  309.       menulist.selectedItem = pixelItem;
  310.     }
  311.   }
  312.   else
  313.     menulist.selectedIndex = defaultIndex;
  314.  
  315.   return size;
  316. }
  317.  
  318. function onAdvancedEdit()
  319. {
  320.   // First validate data from widgets in the "simpler" property dialog
  321.   if (ValidateData())
  322.   {
  323.     // Set true if OK is clicked in the Advanced Edit dialog
  324.     window.AdvancedEditOK = false;
  325.     // Open the AdvancedEdit dialog, passing in the element to be edited
  326.     //  (the copy named "globalElement")
  327.     window.openDialog("chrome://editor/content/EdAdvancedEdit.xul", "_blank", "chrome,close,titlebar,modal,resizable=yes", "", globalElement);
  328.     window.focus();
  329.     if (window.AdvancedEditOK)
  330.     {
  331.       // Copy edited attributes to the dialog widgets:
  332.       InitDialog();
  333.     }
  334.   }
  335. }
  336.  
  337. function getColor(ColorPickerID)
  338. {
  339.   var colorPicker = document.getElementById(ColorPickerID);
  340.   var color;
  341.   if (colorPicker)
  342.   {
  343.     // Extract color from colorPicker and assign to colorWell.
  344.     color = colorPicker.getAttribute("color");
  345.     if (color && color == "")
  346.       return null;
  347.     // Clear color so next if it's called again before
  348.     //  color picker is actually used, we dedect the "don't set color" state
  349.     colorPicker.setAttribute("color","");
  350.   }
  351.  
  352.   return color;
  353. }
  354.  
  355. function setColorWell(ColorWellID, color)
  356. {
  357.   var colorWell = document.getElementById(ColorWellID);
  358.   if (colorWell)
  359.   {
  360.     if (!color || color == "")
  361.     {
  362.       // Don't set color (use default)
  363.       // Trigger change to not show color swatch
  364.       colorWell.setAttribute("default","true");
  365.       // Style in CSS sets "background-color",
  366.       //   but color won't clear unless we do this:
  367.       colorWell.removeAttribute("style");
  368.     }
  369.     else
  370.     {
  371.       colorWell.removeAttribute("default");
  372.       // Use setAttribute so colorwell can be a XUL element, such as button
  373.       colorWell.setAttribute("style", "background-color:"+color);
  374.     }
  375.   }
  376. }
  377.  
  378. function getColorAndSetColorWell(ColorPickerID, ColorWellID)
  379. {
  380.   var color = getColor(ColorPickerID);
  381.   setColorWell(ColorWellID, color);
  382.   return color;
  383. }
  384.  
  385. function InitMoreFewer()
  386. {
  387.   // Set SeeMore bool to the OPPOSITE of the current state,
  388.   //   which is automatically saved by using the 'persist="more"'
  389.   //   attribute on the gDialog.MoreFewerButton button
  390.   //   onMoreFewer will toggle it and redraw the dialog
  391.   SeeMore = (gDialog.MoreFewerButton.getAttribute("more") != "1");
  392.   onMoreFewer();
  393.   gDialog.MoreFewerButton.setAttribute("accesskey",GetString("PropertiesAccessKey"));
  394. }
  395.  
  396. function onMoreFewer()
  397. {
  398.   if (SeeMore)
  399.   {
  400.     gDialog.MoreSection.collapsed = true;
  401.     gDialog.MoreFewerButton.setAttribute("more","0");
  402.     gDialog.MoreFewerButton.setAttribute("label",GetString("MoreProperties"));
  403.     SeeMore = false;
  404.   }
  405.   else
  406.   {
  407.     gDialog.MoreSection.collapsed = false;
  408.     gDialog.MoreFewerButton.setAttribute("more","1");
  409.     gDialog.MoreFewerButton.setAttribute("label",GetString("FewerProperties"));
  410.     SeeMore = true;
  411.   }
  412.   window.sizeToContent();
  413. }
  414.  
  415. function SwitchToValidatePanel()
  416. {
  417.   // no default implementation
  418.   // Only EdTableProps.js currently implements this
  419. }
  420.  
  421. const nsIFilePicker = Components.interfaces.nsIFilePicker;
  422.  
  423. function GetLocalFileURL(filterType)
  424. {
  425.   var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
  426.   var fileType = "html";
  427.  
  428.   if (filterType == "img")
  429.   {
  430.     fp.init(window, GetString("SelectImageFile"), nsIFilePicker.modeOpen);
  431.     fp.appendFilters(nsIFilePicker.filterImages);
  432.     fileType = "image";
  433.   }
  434.   // Current usage of this is in Link dialog,
  435.   //  where we always want HTML first
  436.   else if (filterType.indexOf("html") == 0)
  437.   {
  438.     fp.init(window, GetString("OpenHTMLFile"), nsIFilePicker.modeOpen);
  439.  
  440.     // When loading into Composer, direct user to prefer HTML files and text files,
  441.     //   so we call separately to control the order of the filter list
  442.     fp.appendFilters(nsIFilePicker.filterHTML);
  443.     fp.appendFilters(nsIFilePicker.filterText);
  444.  
  445.     // Link dialog also allows linking to images
  446.     if (filterType.indexOf("img") > 0)
  447.       fp.appendFilters(nsIFilePicker.filterImages);
  448.  
  449.   }
  450.   // Default or last filter is "All Files"
  451.   fp.appendFilters(nsIFilePicker.filterAll);
  452.  
  453.   // set the file picker's current directory to last-opened location saved in prefs
  454.   SetFilePickerDirectory(fp, fileType);
  455.  
  456.  
  457.   /* doesn't handle *.shtml files */
  458.   try {
  459.     var ret = fp.show();
  460.     if (ret == nsIFilePicker.returnCancel)
  461.       return null;
  462.   }
  463.   catch (ex) {
  464.     dump("filePicker.chooseInputFile threw an exception\n");
  465.     return null;
  466.   }
  467.   SaveFilePickerDirectory(fp, fileType);
  468.   
  469.   var fileHandler = GetFileProtocolHandler();
  470.   return fp.file ? fileHandler.getURLSpecFromFile(fp.file) : null;
  471. }
  472.  
  473. function GetMetaElement(name)
  474. {
  475.   if (name)
  476.   {
  477.     name = name.toLowerCase();
  478.     if (name != "")
  479.     {
  480.       var editor = GetCurrentEditor();
  481.       try {
  482.         var metaNodes = editor.document.getElementsByTagName("meta");
  483.         for (var i = 0; i < metaNodes.length; i++)
  484.         {
  485.           var metaNode = metaNodes.item(i);
  486.           if (metaNode && metaNode.getAttribute("name") == name)
  487.             return metaNode;
  488.         }
  489.       } catch (e) {}
  490.     }
  491.   }
  492.   return null;
  493. }
  494.  
  495. function CreateMetaElement(name)
  496. {
  497.   var editor = GetCurrentEditor();
  498.   try {
  499.     var metaElement = editor.createElementWithDefaults("meta");
  500.     metaElement.setAttribute("name", name);
  501.     return metaElement;
  502.   } catch (e) {}
  503.  
  504.   return null;
  505. }
  506.  
  507. function GetHTTPEquivMetaElement(name)
  508. {
  509.   if (name)
  510.   {
  511.     name = name.toLowerCase();
  512.     if (name != "")
  513.     {
  514.       var editor = GetCurrentEditor();
  515.       try {
  516.         var metaNodes = editor.document.getElementsByTagName("meta");
  517.         for (var i = 0; i < metaNodes.length; i++)
  518.         {
  519.           var metaNode = metaNodes.item(i);
  520.           if (metaNode)
  521.           {
  522.             var httpEquiv = metaNode.getAttribute("http-equiv");
  523.             if (httpEquiv && httpEquiv.toLowerCase() == name)
  524.               return metaNode;
  525.           }
  526.         }
  527.       } catch (e) {}
  528.     }
  529.   }
  530.   return null;
  531. }
  532.  
  533. function CreateHTTPEquivMetaElement(name)
  534. {
  535.   var editor = GetCurrentEditor();
  536.   try {
  537.     var metaElement = editor.createElementWithDefaults("meta");
  538.     metaElement.setAttribute("http-equiv", name);
  539.     return metaElement;
  540.   } catch (e) {}
  541.  
  542.   return null;
  543. }
  544.  
  545. function CreateHTTPEquivElement(name)
  546. {
  547.   var editor = GetCurrentEditor();
  548.   try {
  549.     var metaElement = editor.createElementWithDefaults("meta");
  550.     metaElement.setAttribute("http-equiv", name);
  551.     return metaElement;
  552.   } catch (e) {}
  553.  
  554.   return null;
  555. }
  556.  
  557. // Change "content" attribute on a META element,
  558. //   or delete entire element it if content is empty
  559. // This uses undoable editor transactions
  560. function SetMetaElementContent(metaElement, content, insertNew, prepend)
  561. {
  562.   if (metaElement)
  563.   {
  564.     var editor = GetCurrentEditor();
  565.     try {
  566.       if(!content || content == "")
  567.       {
  568.         if (!insertNew)
  569.           editor.deleteNode(metaElement);
  570.       }
  571.       else
  572.       {
  573.         if (insertNew)
  574.         {
  575.           metaElement.setAttribute("content", content);
  576.           if (prepend)
  577.             PrependHeadElement(metaElement);
  578.           else
  579.             AppendHeadElement(metaElement);
  580.         }
  581.         else
  582.           editor.setAttribute(metaElement, "content", content);
  583.       }
  584.     } catch (e) {}
  585.   }
  586. }
  587.  
  588. function GetHeadElement()
  589. {
  590.   var editor = GetCurrentEditor();
  591.   try {
  592.     var headList = editor.document.getElementsByTagName("head");
  593.     return headList.item(0);
  594.   } catch (e) {}
  595.  
  596.   return null;
  597. }
  598.  
  599. function PrependHeadElement(element)
  600. {
  601.   var head = GetHeadElement();
  602.   if (head)
  603.   {
  604.     var editor = GetCurrentEditor();
  605.     try {
  606.       // Use editor's undoable transaction
  607.       // Last param "true" says "don't change the selection"
  608.       editor.insertNode(element, head, 0, true);
  609.     } catch (e) {}
  610.   }
  611. }
  612.  
  613. function AppendHeadElement(element)
  614. {
  615.   var head = GetHeadElement();
  616.   if (head)
  617.   {
  618.     var position = 0;
  619.     if (head.hasChildNodes())
  620.       position = head.childNodes.length;
  621.  
  622.     var editor = GetCurrentEditor();
  623.     try {
  624.       // Use editor's undoable transaction
  625.       // Last param "true" says "don't change the selection"
  626.       editor.insertNode(element, head, position, true);
  627.     } catch (e) {}
  628.   }
  629. }
  630.  
  631. function SetWindowLocation()
  632. {
  633.   gLocation = document.getElementById("location");
  634.   if (gLocation)
  635.   {
  636.     window.screenX = Math.max(0, Math.min(window.opener.screenX + Number(gLocation.getAttribute("offsetX")),
  637.                                           screen.availWidth - window.outerWidth));
  638.     window.screenY = Math.max(0, Math.min(window.opener.screenY + Number(gLocation.getAttribute("offsetY")),
  639.                                           screen.availHeight - window.outerHeight));
  640.   }
  641. }
  642.  
  643. function SaveWindowLocation()
  644. {
  645.   if (gLocation)
  646.   {
  647.     var newOffsetX = window.screenX - window.opener.screenX;
  648.     var newOffsetY = window.screenY - window.opener.screenY;
  649.     gLocation.setAttribute("offsetX", window.screenX - window.opener.screenX);
  650.     gLocation.setAttribute("offsetY", window.screenY - window.opener.screenY);
  651.   }
  652. }
  653.  
  654. function onCancel()
  655. {
  656.   SaveWindowLocation();
  657.   // Close dialog by returning true
  658.   return true;
  659. }
  660.  
  661. function SetRelativeCheckbox(checkbox)
  662. {
  663.   if (!checkbox) {
  664.     checkbox = document.getElementById("MakeRelativeCheckbox");
  665.     if (!checkbox)
  666.       return;
  667.   }
  668.  
  669.   var editor = GetCurrentEditor();
  670.   // Mail never allows relative URLs, so hide the checkbox
  671.   if (editor && (editor.flags & Components.interfaces.nsIPlaintextEditor.eEditorMailMask))
  672.   {
  673.     checkbox.collapsed = true;
  674.     return;
  675.   }
  676.  
  677.   var input =  document.getElementById(checkbox.getAttribute("for"));
  678.   if (!input)
  679.     return;
  680.  
  681.   var url = TrimString(input.value);
  682.   var urlScheme = GetScheme(url);
  683.  
  684.   // Check it if url is relative (no scheme).
  685.   checkbox.checked = url.length > 0 && !urlScheme;
  686.  
  687.   // Now do checkbox enabling:
  688.   var enable = false;
  689.  
  690.   var docUrl = GetDocumentBaseUrl();
  691.   var docScheme = GetScheme(docUrl);
  692.  
  693.   if (url && docUrl && docScheme)
  694.   {
  695.     if (urlScheme)
  696.     {
  697.       // Url is absolute
  698.       // If we can make a relative URL, then enable must be true!
  699.       // (this lets the smarts of MakeRelativeUrl do all the hard work)
  700.       enable = (GetScheme(MakeRelativeUrl(url)).length == 0);
  701.     }
  702.     else
  703.     {
  704.       // Url is relative
  705.       // Check if url is a named anchor
  706.       //  but document doesn't have a filename
  707.       // (it's probably "index.html" or "index.htm",
  708.       //  but we don't want to allow a malformed URL)
  709.       if (url[0] == "#")
  710.       {
  711.         var docFilename = GetFilename(docUrl);
  712.         enable = docFilename.length > 0;
  713.       }
  714.       else
  715.       {
  716.         // Any other url is assumed 
  717.         //  to be ok to try to make absolute
  718.         enable = true;
  719.       }
  720.     }
  721.   }
  722.  
  723.   SetElementEnabled(checkbox, enable);
  724. }
  725.  
  726. // oncommand handler for the Relativize checkbox in EditorOverlay.xul
  727. function MakeInputValueRelativeOrAbsolute(checkbox)
  728. {
  729.   var input =  document.getElementById(checkbox.getAttribute("for"));
  730.   if (!input)
  731.     return;
  732.  
  733.   var docUrl = GetDocumentBaseUrl();
  734.   if (!docUrl)
  735.   {
  736.     // Checkbox should be disabled if not saved,
  737.     //  but keep this error message in case we change that
  738.     AlertWithTitle("", GetString("SaveToUseRelativeUrl"));
  739.     window.focus();
  740.   }
  741.   else 
  742.   {
  743.     // Note that "checked" is opposite of its last state,
  744.     //  which determines what we want to do here
  745.     if (checkbox.checked)
  746.       input.value = MakeRelativeUrl(input.value);
  747.     else
  748.       input.value = MakeAbsoluteUrl(input.value);
  749.  
  750.     // Reset checkbox to reflect url state
  751.     SetRelativeCheckbox(checkbox);
  752.   }
  753. }
  754.  
  755. var IsBlockParent = {
  756.   APPLET: true,
  757.   BLOCKQUOTE: true,
  758.   BODY: true,
  759.   CENTER: true,
  760.   DD: true,
  761.   DIV: true,
  762.   FORM: true,
  763.   LI: true,
  764.   NOSCRIPT: true,
  765.   OBJECT: true,
  766.   TD: true,
  767.   TH: true
  768. };
  769.  
  770. var NotAnInlineParent = {
  771.   COL: true,
  772.   COLGROUP: true,
  773.   DL: true,
  774.   DIR: true,
  775.   MENU: true,
  776.   OL: true,
  777.   TABLE: true,
  778.   TBODY: true,
  779.   TFOOT: true,
  780.   THEAD: true,
  781.   TR: true,
  782.   UL: true
  783. };
  784.  
  785. function nodeIsBreak(editor, node)
  786. {
  787.   return !node || node.localName == 'BR' || editor.nodeIsBlock(node);
  788. }
  789.  
  790. function InsertElementAroundSelection(element)
  791. {
  792.   var editor = GetCurrentEditor();
  793.   editor.beginTransaction();
  794.  
  795.   try {
  796.     // First get the selection as a single range
  797.     var range, start, end, offset;
  798.     var count = editor.selection.rangeCount;
  799.     if (count == 1)
  800.       range = editor.selection.getRangeAt(0).cloneRange();
  801.     else
  802.     {
  803.       range = editor.document.createRange();
  804.       start = editor.selection.getRangeAt(0)
  805.       range.setStart(start.startContainer, start.startOffset);
  806.       end = editor.selection.getRangeAt(--count);
  807.       range.setEnd(end.endContainer, end.endOffset);
  808.     }
  809.  
  810.     // Flatten the selection to child nodes of the common ancestor
  811.     while (range.startContainer != range.commonAncestorContainer)
  812.       range.setStartBefore(range.startContainer);
  813.     while (range.endContainer != range.commonAncestorContainer)
  814.       range.setEndAfter(range.endContainer);
  815.  
  816.     if (editor.nodeIsBlock(element))
  817.       // Block element parent must be a valid block
  818.       while (!(range.commonAncestorContainer.localName in IsBlockParent))
  819.         range.selectNode(range.commonAncestorContainer);
  820.     else
  821.     {
  822.       // Fail if we're not inserting a block (use setInlineProperty instead)
  823.       if (!nodeIsBreak(editor, range.commonAncestorContainer))
  824.         return false;
  825.       else if (range.commonAncestorContainer.localName in NotAnInlineParent)
  826.         // Inline element parent must not be an invalid block
  827.         do range.selectNode(range.commonAncestorContainer);
  828.         while (range.commonAncestorContainer.localName in NotAnInlineParent);
  829.       else
  830.         // Further insert block check
  831.         for (var i = range.startOffset; ; i++)
  832.           if (i == range.endOffset)
  833.             return false;
  834.           else if (nodeIsBreak(editor, range.commonAncestorContainer.childNodes[i]))
  835.             break;
  836.     }
  837.  
  838.     // The range may be contained by body text, which should all be selected.
  839.     offset = range.startOffset;
  840.     start = range.startContainer.childNodes[offset];
  841.     if (!nodeIsBreak(editor, start))
  842.     {
  843.       while (!nodeIsBreak(editor, start.previousSibling))
  844.       {
  845.         start = start.previousSibling;
  846.         offset--;
  847.       }
  848.     }
  849.     end = range.endContainer.childNodes[range.endOffset];
  850.     if (end && !nodeIsBreak(editor, end.previousSibling))
  851.     {
  852.       while (!nodeIsBreak(editor, end))
  853.         end = end.nextSibling;
  854.     }
  855.  
  856.     // Now insert the node
  857.     editor.insertNode(element, range.commonAncestorContainer, offset, true);
  858.     offset = element.childNodes.length;
  859.     if (!editor.nodeIsBlock(element))
  860.       editor.setShouldTxnSetSelection(false);
  861.  
  862.     // Move all the old child nodes to the element
  863.     var empty = true;
  864.     while (start != end)
  865.     {
  866.       var next = start.nextSibling;
  867.       editor.deleteNode(start);
  868.       editor.insertNode(start, element, element.childNodes.length);
  869.       empty = false;
  870.       start = next;
  871.     }
  872.     if (!editor.nodeIsBlock(element))
  873.       editor.setShouldTxnSetSelection(true);
  874.     else
  875.     {
  876.       // Also move a trailing <br>
  877.       if (start && start.localName == 'BR')
  878.       {
  879.         editor.deleteNode(start);
  880.         editor.insertNode(start, element, element.childNodes.length);
  881.         empty = false;
  882.       }
  883.       // Still nothing? Insert a <br> so the node is not empty
  884.       if (empty)
  885.         editor.insertNode(editor.createElementWithDefaults("br"), element, element.childNodes.length);
  886.  
  887.       // Hack to set the selection just inside the element
  888.       editor.insertNode(editor.document.createTextNode(""), element, offset);
  889.     }
  890.   }
  891.   finally {
  892.     editor.endTransaction();
  893.   }
  894.  
  895.   return true;
  896. }
  897.  
  898. function nodeIsBlank(node)
  899. {
  900.   return node && node.NODE_TYPE == Node.TEXT_NODE && !/\S/.test(node.data);
  901. }
  902.  
  903. function nodeBeginsBlock(node)
  904. {
  905.   while (nodeIsBlank(node))
  906.     node = node.nextSibling;
  907.   return nodeIsBlock(node);
  908. }
  909.  
  910. function nodeEndsBlock(node)
  911. {
  912.   while (nodeIsBlank(node))
  913.     node = node.previousSibling;
  914.   return nodeIsBlock(node);
  915. }
  916.  
  917. // C++ function isn't exposed to JS :-(
  918. function RemoveBlockContainer(element)
  919. {
  920.   var editor = GetCurrentEditor();
  921.   editor.beginTransaction();
  922.  
  923.   try {
  924.     var range = editor.document.createRange();
  925.     range.selectNode(element);
  926.     var offset = range.startOffset;
  927.     var parent = element.parentNode;
  928.  
  929.     // May need to insert a break after the removed element
  930.     if (!nodeBeginsBlock(editor, element.nextSibling) &&
  931.         !nodeEndsBlock(editor, element.lastChild))
  932.       editor.insertNode(editor.createElementWithDefaults("br"), parent, range.endOffset);
  933.  
  934.     // May need to insert a break before the removed element, or if it was empty
  935.     if (!nodeEndsBlock(editor, element.previousSibling) &&
  936.         !nodeBeginsBlock(editor, element.firstChild || element.nextSibling))
  937.       editor.insertNode(editor.createElementWithDefaults("br"), parent, offset++);
  938.  
  939.     // Now remove the element
  940.     editor.deleteNode(element);
  941.  
  942.     // Need to copy the contained nodes?
  943.     for (var i = 0; i < element.childNodes.length; i++)
  944.       editor.insertNode(element.childNodes[i].cloneNode(true), parent, offset++);
  945.   }
  946.   finally {
  947.     editor.endTransaction();
  948.   }
  949. }
  950.  
  951. // C++ function isn't exposed to JS :-(
  952. function RemoveContainer(element)
  953. {
  954.   var editor = GetCurrentEditor();
  955.   editor.beginTransaction();
  956.  
  957.   try {
  958.     var range = editor.document.createRange();
  959.     var parent = element.parentNode;
  960.     // Allow for automatic joining of text nodes
  961.     // so we can't delete the container yet
  962.     // so we need to copy the contained nodes
  963.     for (var i = 0; i < element.childNodes.length; i++) {
  964.       range.selectNode(element);
  965.       editor.insertNode(element.childNodes[i].cloneNode(true), parent, range.startOffset);
  966.     }
  967.     // Now remove the element
  968.     editor.deleteNode(element);
  969.   }
  970.   finally {
  971.     editor.endTransaction();
  972.   }
  973. }
  974.  
  975. function FillLinkMenulist(linkMenulist, headingsArray)
  976. {
  977.   var menupopup = linkMenulist.firstChild;
  978.   var editor = GetCurrentEditor();
  979.   try {
  980.     var NamedAnchorNodeList = editor.document.anchors;
  981.     var NamedAnchorCount = NamedAnchorNodeList.length;
  982.     if (NamedAnchorCount > 0)
  983.     {
  984.       for (var i = 0; i < NamedAnchorCount; i++)
  985.         createMenuItem(menupopup, "#" + NamedAnchorNodeList.item(i).name);
  986.     } 
  987.     for (var j = 1; j <= 6; j++)
  988.     {
  989.       var headingList = editor.document.getElementsByTagName("h" + j);
  990.       for (var k = 0; k < headingList.length; k++)
  991.       {
  992.         var heading = headingList.item(k);
  993.  
  994.         // Skip headings that already have a named anchor as their first child
  995.         //  (this may miss nearby anchors, but at least we don't insert another
  996.         //   under the same heading)
  997.         var child = heading.firstChild;
  998.         if (child && child.nodeName == "A" && child.name && (child.name.length>0))
  999.           continue;
  1000.  
  1001.         var range = editor.document.createRange();
  1002.         range.setStart(heading,0);
  1003.         var lastChildIndex = heading.childNodes.length;
  1004.         range.setEnd(heading,lastChildIndex);
  1005.         var text = range.toString();
  1006.         if (text)
  1007.         {
  1008.           // Use just first 40 characters, don't add "...",
  1009.           //  and replace whitespace with "_" and strip non-word characters
  1010.           text = "#" + ConvertToCDATAString(TruncateStringAtWordEnd(text, 40, false));
  1011.           // Append "_" to any name already in the list
  1012.           while (linkMenulist.getElementsByAttribute("label", text).length)
  1013.             text += "_";
  1014.           createMenuItem(menupopup, text);
  1015.  
  1016.           // Save nodes in an array so we can create anchor node under it later
  1017.           headingsArray[text] = heading;
  1018.         }
  1019.       }
  1020.     }
  1021.     if (!menupopup.hasChildNodes()) 
  1022.     {
  1023.       var item = createMenuItem(menupopup, GetString("NoNamedAnchorsOrHeadings"));
  1024.       item.setAttribute("disabled", "true");
  1025.     }
  1026.   } catch (e) {}
  1027. }
  1028.  
  1029. function createMenuItem(aMenuPopup, aLabel)
  1030. {
  1031.   var menuitem = document.createElement("menuitem");
  1032.   menuitem.setAttribute("label", aLabel);
  1033.   aMenuPopup.appendChild(menuitem);
  1034.   return menuitem;
  1035. }
  1036.  
  1037. // Shared by Image and Link dialogs for the "Choose" button for links
  1038. function chooseLinkFile()
  1039. {
  1040.   // Get a local file, converted into URL format
  1041.   var fileName = GetLocalFileURL("html, img");
  1042.   if (fileName) 
  1043.   {
  1044.     // Always try to relativize local file URLs
  1045.     if (gHaveDocumentUrl)
  1046.       fileName = MakeRelativeUrl(fileName);
  1047.  
  1048.     gDialog.hrefInput.value = fileName;
  1049.  
  1050.     // Do stuff specific to a particular dialog
  1051.     // (This is defined separately in Image and Link dialogs)
  1052.     ChangeLinkLocation();
  1053.   }
  1054.   // Put focus into the input field
  1055.   SetTextboxFocus(gDialog.hrefInput);
  1056. }
  1057.  
  1058.