home *** CD-ROM | disk | FTP | other *** search
/ Chip 2004 April / CMCD0404.ISO / Software / Freeware / Programare / groupoffice-com-2.01 / controls / htmlarea / htmlarea.js < prev    next >
Encoding:
JavaScript  |  2004-03-08  |  68.2 KB  |  2,210 lines

  1. // htmlArea v3.0 - Copyright (c) 2002-2004 interactivetools.com, inc.
  2. // This copyright notice MUST stay intact for use (see license.txt).
  3. //
  4. // Portions (c) dynarch.com, 2003-2004
  5. //
  6. // A free WYSIWYG editor replacement for <textarea> fields.
  7. // For full source code and docs, visit http://www.interactivetools.com/
  8. //
  9. // Version 3.0 developed by Mihai Bazon.
  10. //   http://dynarch.com/mishoo
  11. //
  12. // $Id: htmlarea.js,v 1.21 2004/03/03 11:22:40 mschering Exp $
  13.  
  14. if (typeof _editor_url == "string") {
  15.     // Leave exactly one backslash at the end of _editor_url
  16.     _editor_url = _editor_url.replace(/\x2f*$/, '/');
  17. } else {
  18.     alert("WARNING: _editor_url is not set!  You should set this variable to the editor files path; it should preferably be an absolute path, like in '/htmlarea/', but it can be relative if you prefer.  Further we will try to load the editor files correctly but we'll probably fail.");
  19.     _editor_url = '';
  20. }
  21.  
  22. // make sure we have a language
  23. if (typeof _editor_lang == "string") {
  24.     _editor_lang = _editor_lang.toLowerCase();
  25. } else {
  26.     _editor_lang = "en";
  27. }
  28.  
  29. // Creates a new HTMLArea object.  Tries to replace the textarea with the given
  30. // ID with it.
  31. function HTMLArea(textarea, config) {
  32.     if (HTMLArea.checkSupportedBrowser()) {
  33.         if (typeof config == "undefined") {
  34.             this.config = new HTMLArea.Config();
  35.         } else {
  36.             this.config = config;
  37.         }
  38.         this._htmlArea = null;
  39.         this._textArea = textarea;
  40.         this._editMode = "wysiwyg";
  41.         this.plugins = {};
  42.         this._timerToolbar = null;
  43.         this._timerUndo = null;
  44.         this._undoQueue = new Array(this.config.undoSteps);
  45.         this._undoPos = -1;
  46.         this._customUndo = false;
  47.         this._mdoc = document; // cache the document, we need it in plugins
  48.         this.doctype = '';
  49.     }
  50. };
  51.  
  52. // load some scripts
  53. (function() {
  54.     var scripts = HTMLArea._scripts = [ _editor_url + "htmlarea.js",
  55.                         _editor_url + "dialog.js",
  56.                         _editor_url + "popupwin.js",
  57.                         _editor_url + "lang/" + _editor_lang + ".js" ];
  58.     var head = document.getElementsByTagName("head")[0];
  59.     // start from 1, htmlarea.js is already loaded
  60.     for (var i = 1; i < scripts.length; ++i) {
  61.         var script = document.createElement("script");
  62.         script.src = scripts[i];
  63.         head.appendChild(script);
  64.     }
  65. })();
  66.  
  67. // cache some regexps
  68. HTMLArea.RE_tagName = /(<\/|<)\s*([^ \t\n>]+)/ig;
  69. HTMLArea.RE_doctype = /(<!doctype((.|\n)*?)>)\n?/i;
  70. HTMLArea.RE_head    = /<head>((.|\n)*?)<\/head>/i;
  71. HTMLArea.RE_body    = /<body>((.|\n)*?)<\/body>/i;
  72.  
  73. HTMLArea.Config = function () {
  74.     this.version = "3.0";
  75.  
  76.     this.width = "auto";
  77.     this.height = "auto";
  78.  
  79.     // enable creation of a status bar?
  80.     this.statusBar = true;
  81.  
  82.     // maximum size of the undo queue
  83.     this.undoSteps = 20;
  84.  
  85.     // the time interval at which undo samples are taken
  86.     this.undoTimeout = 500;    // 1/2 sec.
  87.  
  88.     // the next parameter specifies whether the toolbar should be included
  89.     // in the size or not.
  90.     this.sizeIncludesToolbar = true;
  91.  
  92.     // if true then HTMLArea will retrieve the full HTML, starting with the
  93.     // <HTML> tag.
  94.     this.fullPage = false;
  95.  
  96.     // style included in the iframe document
  97.     this.pageStyle = "";
  98.  
  99.     // set to true if you want Word code to be cleaned upon Paste
  100.     this.killWordOnPaste = false;
  101.  
  102.     // BaseURL included in the iframe document
  103.     this.baseURL = document.baseURI || document.URL;
  104.     if (this.baseURL && this.baseURL.match(/(.*)\/([^\/]+)/))
  105.         this.baseURL = RegExp.$1 + "/";
  106.  
  107.     // URL-s
  108.     this.imgURL = "images/";
  109.     this.popupURL = "popups/";
  110.  
  111.     /** CUSTOMIZING THE TOOLBAR
  112.      * -------------------------
  113.      *
  114.      * It is recommended that you customize the toolbar contents in an
  115.      * external file (i.e. the one calling HTMLArea) and leave this one
  116.      * unchanged.  That's because when we (InteractiveTools.com) release a
  117.      * new official version, it's less likely that you will have problems
  118.      * upgrading HTMLArea.
  119.      */
  120.     this.toolbar = [
  121.         [ "fontname", "space",
  122.           "fontsize", "space",
  123.           "formatblock", "space",
  124.           "bold", "italic", "underline", "strikethrough", "separator",
  125.           "subscript", "superscript", "separator",
  126.           "copy", "cut", "paste", "space", "undo", "redo" ],
  127.  
  128.         [ "justifyleft", "justifycenter", "justifyright", "justifyfull", "separator",
  129.           "lefttoright", "righttoleft", "separator",
  130.           "insertorderedlist", "insertunorderedlist", "outdent", "indent", "separator",
  131.           "forecolor", "hilitecolor", "separator",
  132.           "inserthorizontalrule", "createlink", "insertimage", "inserttable", "htmlmode", "separator",
  133.           "popupeditor", "separator", "showhelp", "about" ]
  134.     ];
  135.  
  136.     this.fontname = {
  137.         "Arial":       'arial,helvetica,sans-serif',
  138.         "Courier New":       'courier new,courier,monospace',
  139.         "Georgia":       'georgia,times new roman,times,serif',
  140.         "Tahoma":       'tahoma,arial,helvetica,sans-serif',
  141.         "Times New Roman": 'times new roman,times,serif',
  142.         "Verdana":       'verdana,arial,helvetica,sans-serif',
  143.         "impact":       'impact',
  144.         "WingDings":       'wingdings'
  145.     };
  146.  
  147.     this.fontsize = {
  148.         "1 (8 pt)":  "1",
  149.         "2 (10 pt)": "2",
  150.         "3 (12 pt)": "3",
  151.         "4 (14 pt)": "4",
  152.         "5 (18 pt)": "5",
  153.         "6 (24 pt)": "6",
  154.         "7 (36 pt)": "7"
  155.     };
  156.  
  157.     this.formatblock = {
  158.         "Heading 1": "h1",
  159.         "Heading 2": "h2",
  160.         "Heading 3": "h3",
  161.         "Heading 4": "h4",
  162.         "Heading 5": "h5",
  163.         "Heading 6": "h6",
  164.         "Normal": "p",
  165.         "Address": "address",
  166.         "Formatted": "pre"
  167.     };
  168.  
  169.     this.customSelects = {};
  170.  
  171.     function cut_copy_paste(e, cmd, obj) {
  172.         e.execCommand(cmd);
  173.     };
  174.  
  175.     // ADDING CUSTOM BUTTONS: please read below!
  176.     // format of the btnList elements is "ID: [ ToolTip, Icon, Enabled in text mode?, ACTION ]"
  177.     //    - ID: unique ID for the button.  If the button calls document.execCommand
  178.     //        it's wise to give it the same name as the called command.
  179.     //    - ACTION: function that gets called when the button is clicked.
  180.     //              it has the following prototype:
  181.     //                 function(editor, buttonName)
  182.     //              - editor is the HTMLArea object that triggered the call
  183.     //              - buttonName is the ID of the clicked button
  184.     //              These 2 parameters makes it possible for you to use the same
  185.     //              handler for more HTMLArea objects or for more different buttons.
  186.     //    - ToolTip: default tooltip, for cases when it is not defined in the -lang- file (HTMLArea.I18N)
  187.     //    - Icon: path to an icon image file for the button (TODO: use one image for all buttons!)
  188.     //    - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time.
  189.     this.btnList = {
  190.         bold: [ "Bold", "ed_format_bold.gif", false, function(e) {e.execCommand("bold");} ],
  191.         italic: [ "Italic", "ed_format_italic.gif", false, function(e) {e.execCommand("italic");} ],
  192.         underline: [ "Underline", "ed_format_underline.gif", false, function(e) {e.execCommand("underline");} ],
  193.         strikethrough: [ "Strikethrough", "ed_format_strike.gif", false, function(e) {e.execCommand("strikethrough");} ],
  194.         subscript: [ "Subscript", "ed_format_sub.gif", false, function(e) {e.execCommand("subscript");} ],
  195.         superscript: [ "Superscript", "ed_format_sup.gif", false, function(e) {e.execCommand("superscript");} ],
  196.         justifyleft: [ "Justify Left", "ed_align_left.gif", false, function(e) {e.execCommand("justifyleft");} ],
  197.         justifycenter: [ "Justify Center", "ed_align_center.gif", false, function(e) {e.execCommand("justifycenter");} ],
  198.         justifyright: [ "Justify Right", "ed_align_right.gif", false, function(e) {e.execCommand("justifyright");} ],
  199.         justifyfull: [ "Justify Full", "ed_align_justify.gif", false, function(e) {e.execCommand("justifyfull");} ],
  200.         insertorderedlist: [ "Ordered List", "ed_list_num.gif", false, function(e) {e.execCommand("insertorderedlist");} ],
  201.         insertunorderedlist: [ "Bulleted List", "ed_list_bullet.gif", false, function(e) {e.execCommand("insertunorderedlist");} ],
  202.         outdent: [ "Decrease Indent", "ed_indent_less.gif", false, function(e) {e.execCommand("outdent");} ],
  203.         indent: [ "Increase Indent", "ed_indent_more.gif", false, function(e) {e.execCommand("indent");} ],
  204.         forecolor: [ "Font Color", "ed_color_fg.gif", false, function(e) {e.execCommand("forecolor");} ],
  205.         hilitecolor: [ "Background Color", "ed_color_bg.gif", false, function(e) {e.execCommand("hilitecolor");} ],
  206.         inserthorizontalrule: [ "Horizontal Rule", "ed_hr.gif", false, function(e) {e.execCommand("inserthorizontalrule");} ],
  207.         createlink: [ "Insert Web Link", "ed_link.gif", false, function(e) {e.execCommand("createlink", true);} ],
  208.         insertimage: [ "Insert/Modify Image", "ed_image.gif", false, function(e) {e.execCommand("insertimage");} ],
  209.         inserttable: [ "Insert Table", "insert_table.gif", false, function(e) {e.execCommand("inserttable");} ],
  210.         htmlmode: [ "Toggle HTML Source", "ed_html.gif", true, function(e) {e.execCommand("htmlmode");} ],
  211.         popupeditor: [ "Enlarge Editor", "fullscreen_maximize.gif", true, function(e) {e.execCommand("popupeditor");} ],
  212.         about: [ "About this editor", "ed_about.gif", true, function(e) {e.execCommand("about");} ],
  213.         showhelp: [ "Help using editor", "ed_help.gif", true, function(e) {e.execCommand("showhelp");} ],
  214.         undo: [ "Undoes your last action", "ed_undo.gif", false, function(e) {e.execCommand("undo");} ],
  215.         redo: [ "Redoes your last action", "ed_redo.gif", false, function(e) {e.execCommand("redo");} ],
  216.         cut: [ "Cut selection", "ed_cut.gif", false, cut_copy_paste ],
  217.         copy: [ "Copy selection", "ed_copy.gif", false, cut_copy_paste ],
  218.         paste: [ "Paste from clipboard", "ed_paste.gif", false, cut_copy_paste ],
  219.         lefttoright: [ "Direction left to right", "ed_left_to_right.gif", false, function(e) {e.execCommand("lefttoright");} ],
  220.         righttoleft: [ "Direction right to left", "ed_right_to_left.gif", false, function(e) {e.execCommand("righttoleft");} ]
  221.     };
  222.     /* ADDING CUSTOM BUTTONS
  223.      * ---------------------
  224.      *
  225.      * It is recommended that you add the custom buttons in an external
  226.      * file and leave this one unchanged.  That's because when we
  227.      * (InteractiveTools.com) release a new official version, it's less
  228.      * likely that you will have problems upgrading HTMLArea.
  229.      *
  230.      * Example on how to add a custom button when you construct the HTMLArea:
  231.      *
  232.      *   var editor = new HTMLArea("your_text_area_id");
  233.      *   var cfg = editor.config; // this is the default configuration
  234.      *   cfg.btnList["my-hilite"] =
  235.      *    [ function(editor) { editor.surroundHTML('<span style="background:yellow">', '</span>'); }, // action
  236.      *      "Highlight selection", // tooltip
  237.      *      "my_hilite.gif", // image
  238.      *      false // disabled in text mode
  239.      *    ];
  240.      *   cfg.toolbar.push(["linebreak", "my-hilite"]); // add the new button to the toolbar
  241.      *
  242.      * An alternate (also more convenient and recommended) way to
  243.      * accomplish this is to use the registerButton function below.
  244.      */
  245.     // initialize tooltips from the I18N module and generate correct image path
  246.     for (var i in this.btnList) {
  247.         var btn = this.btnList[i];
  248.         btn[1] = _editor_url + this.imgURL + btn[1];
  249.         if (typeof HTMLArea.I18N.tooltips[i] != "undefined") {
  250.             btn[0] = HTMLArea.I18N.tooltips[i];
  251.         }
  252.     }
  253. };
  254.  
  255. /** Helper function: register a new button with the configuration.  It can be
  256.  * called with all 5 arguments, or with only one (first one).  When called with
  257.  * only one argument it must be an object with the following properties: id,
  258.  * tooltip, image, textMode, action.  Examples:
  259.  *
  260.  * 1. config.registerButton("my-hilite", "Hilite text", "my-hilite.gif", false, function(editor) {...});
  261.  * 2. config.registerButton({
  262.  *      id       : "my-hilite",      // the ID of your button
  263.  *      tooltip  : "Hilite text",    // the tooltip
  264.  *      image    : "my-hilite.gif",  // image to be displayed in the toolbar
  265.  *      textMode : false,            // disabled in text mode
  266.  *      action   : function(editor) { // called when the button is clicked
  267.  *                   editor.surroundHTML('<span class="hilite">', '</span>');
  268.  *                 },
  269.  *      context  : "p"               // will be disabled if outside a <p> element
  270.  *    });
  271.  */
  272. HTMLArea.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context) {
  273.     var the_id;
  274.     if (typeof id == "string") {
  275.         the_id = id;
  276.     } else if (typeof id == "object") {
  277.         the_id = id.id;
  278.     } else {
  279.         alert("ERROR [HTMLArea.Config::registerButton]:\ninvalid arguments");
  280.         return false;
  281.     }
  282.     // check for existing id
  283.     if (typeof this.customSelects[the_id] != "undefined") {
  284.         // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
  285.     }
  286.     if (typeof this.btnList[the_id] != "undefined") {
  287.         // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
  288.     }
  289.     switch (typeof id) {
  290.         case "string": this.btnList[id] = [ tooltip, image, textMode, action, context ]; break;
  291.         case "object": this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; break;
  292.     }
  293. };
  294.  
  295. /** The following helper function registers a dropdown box with the editor
  296.  * configuration.  You still have to add it to the toolbar, same as with the
  297.  * buttons.  Call it like this:
  298.  *
  299.  * FIXME: add example
  300.  */
  301. HTMLArea.Config.prototype.registerDropdown = function(object) {
  302.     // check for existing id
  303.     if (typeof this.customSelects[object.id] != "undefined") {
  304.         // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
  305.     }
  306.     if (typeof this.btnList[object.id] != "undefined") {
  307.         // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
  308.     }
  309.     this.customSelects[object.id] = object;
  310. };
  311.  
  312. /** Call this function to remove some buttons/drop-down boxes from the toolbar.
  313.  * Pass as the only parameter a string containing button/drop-down names
  314.  * delimited by spaces.  Note that the string should also begin with a space
  315.  * and end with a space.  Example:
  316.  *
  317.  *   config.hideSomeButtons(" fontname fontsize textindicator ");
  318.  *
  319.  * It's useful because it's easier to remove stuff from the defaul toolbar than
  320.  * create a brand new toolbar ;-)
  321.  */
  322. HTMLArea.Config.prototype.hideSomeButtons = function(remove) {
  323.     var toolbar = this.toolbar;
  324.     for (var i in toolbar) {
  325.         var line = toolbar[i];
  326.         for (var j = line.length; --j >= 0; ) {
  327.             if (remove.indexOf(" " + line[j] + " ") >= 0) {
  328.                 var len = 1;
  329.                 if (/separator|space/.test(line[j + 1])) {
  330.                     len = 2;
  331.                 }
  332.                 line.splice(j, len);
  333.             }
  334.         }
  335.     }
  336. };
  337.  
  338. /** Helper function: replace all TEXTAREA-s in the document with HTMLArea-s. */
  339. HTMLArea.replaceAll = function(config) {
  340.     var tas = document.getElementsByTagName("textarea");
  341.     for (var i = tas.length; i > 0; (new HTMLArea(tas[--i], config)).generate());
  342. };
  343.  
  344. /** Helper function: replaces the TEXTAREA with the given ID with HTMLArea. */
  345. HTMLArea.replace = function(id, config) {
  346.     var ta = HTMLArea.getElementById("textarea", id);
  347.     return ta ? (new HTMLArea(ta, config)).generate() : null;
  348. };
  349.  
  350. // Creates the toolbar and appends it to the _htmlarea
  351. HTMLArea.prototype._createToolbar = function () {
  352.     var editor = this;    // to access this in nested functions
  353.  
  354.     var toolbar = document.createElement("div");
  355.     this._toolbar = toolbar;
  356.     toolbar.className = "toolbar";
  357.     toolbar.unselectable = "1";
  358.     var tb_row = null;
  359.     var tb_objects = new Object();
  360.     this._toolbarObjects = tb_objects;
  361.  
  362.     // creates a new line in the toolbar
  363.     function newLine() {
  364.         var table = document.createElement("table");
  365.         table.border = "0px";
  366.         table.cellSpacing = "0px";
  367.         table.cellPadding = "0px";
  368.         toolbar.appendChild(table);
  369.         // TBODY is required for IE, otherwise you don't see anything
  370.         // in the TABLE.
  371.         var tb_body = document.createElement("tbody");
  372.         table.appendChild(tb_body);
  373.         tb_row = document.createElement("tr");
  374.         tb_body.appendChild(tb_row);
  375.     }; // END of function: newLine
  376.     // init first line
  377.     newLine();
  378.  
  379.     // updates the state of a toolbar element.  This function is member of
  380.     // a toolbar element object (unnamed objects created by createButton or
  381.     // createSelect functions below).
  382.     function setButtonStatus(id, newval) {
  383.         var oldval = this[id];
  384.         var el = this.element;
  385.         if (oldval != newval) {
  386.             switch (id) {
  387.                 case "enabled":
  388.                 if (newval) {
  389.                     HTMLArea._removeClass(el, "buttonDisabled");
  390.                     el.disabled = false;
  391.                 } else {
  392.                     HTMLArea._addClass(el, "buttonDisabled");
  393.                     el.disabled = true;
  394.                 }
  395.                 break;
  396.                 case "active":
  397.                 if (newval) {
  398.                     HTMLArea._addClass(el, "buttonPressed");
  399.                 } else {
  400.                     HTMLArea._removeClass(el, "buttonPressed");
  401.                 }
  402.                 break;
  403.             }
  404.             this[id] = newval;
  405.         }
  406.     }; // END of function: setButtonStatus
  407.  
  408.     // this function will handle creation of combo boxes.  Receives as
  409.     // parameter the name of a button as defined in the toolBar config.
  410.     // This function is called from createButton, above, if the given "txt"
  411.     // doesn't match a button.
  412.     function createSelect(txt) {
  413.         var options = null;
  414.         var el = null;
  415.         var cmd = null;
  416.         var customSelects = editor.config.customSelects;
  417.         var context = null;
  418.         switch (txt) {
  419.             case "fontsize":
  420.             case "fontname":
  421.             case "formatblock":
  422.             // the following line retrieves the correct
  423.             // configuration option because the variable name
  424.             // inside the Config object is named the same as the
  425.             // button/select in the toolbar.  For instance, if txt
  426.             // == "formatblock" we retrieve config.formatblock (or
  427.             // a different way to write it in JS is
  428.             // config["formatblock"].
  429.             options = editor.config[txt];
  430.             cmd = txt;
  431.             break;
  432.             default:
  433.             // try to fetch it from the list of registered selects
  434.             cmd = txt;
  435.             var dropdown = customSelects[cmd];
  436.             if (typeof dropdown != "undefined") {
  437.                 options = dropdown.options;
  438.                 context = dropdown.context;
  439.             } else {
  440.                 alert("ERROR [createSelect]:\nCan't find the requested dropdown definition");
  441.             }
  442.             break;
  443.         }
  444.         if (options) {
  445.             el = document.createElement("select");
  446.             var obj = {
  447.                 name    : txt, // field name
  448.                 element : el,    // the UI element (SELECT)
  449.                 enabled : true, // is it enabled?
  450.                 text    : false, // enabled in text mode?
  451.                 cmd    : cmd, // command ID
  452.                 state    : setButtonStatus, // for changing state
  453.                 context : context
  454.             };
  455.             tb_objects[txt] = obj;
  456.             for (var i in options) {
  457.                 var op = document.createElement("option");
  458.                 op.appendChild(document.createTextNode(i));
  459.                 op.value = options[i];
  460.                 el.appendChild(op);
  461.             }
  462.             HTMLArea._addEvent(el, "change", function () {
  463.                 editor._comboSelected(el, txt);
  464.             });
  465.         }
  466.         return el;
  467.     }; // END of function: createSelect
  468.  
  469.     // appends a new button to toolbar
  470.     function createButton(txt) {
  471.         // the element that will be created
  472.         var el = null;
  473.         var btn = null;
  474.         switch (txt) {
  475.             case "separator":
  476.             el = document.createElement("div");
  477.             el.className = "separator";
  478.             break;
  479.             case "space":
  480.             el = document.createElement("div");
  481.             el.className = "space";
  482.             break;
  483.             case "linebreak":
  484.             newLine();
  485.             return false;
  486.             case "textindicator":
  487.             el = document.createElement("div");
  488.             el.appendChild(document.createTextNode("A"));
  489.             el.className = "indicator";
  490.             el.title = HTMLArea.I18N.tooltips.textindicator;
  491.             var obj = {
  492.                 name    : txt, // the button name (i.e. 'bold')
  493.                 element : el, // the UI element (DIV)
  494.                 enabled : true, // is it enabled?
  495.                 active    : false, // is it pressed?
  496.                 text    : false, // enabled in text mode?
  497.                 cmd    : "textindicator", // the command ID
  498.                 state    : setButtonStatus // for changing state
  499.             };
  500.             tb_objects[txt] = obj;
  501.             break;
  502.             default:
  503.             btn = editor.config.btnList[txt];
  504.         }
  505.         if (!el && btn) {
  506.             el = document.createElement("div");
  507.             el.title = btn[0];
  508.             el.className = "button";
  509.             // let's just pretend we have a button object, and
  510.             // assign all the needed information to it.
  511.             var obj = {
  512.                 name    : txt, // the button name (i.e. 'bold')
  513.                 element : el, // the UI element (DIV)
  514.                 enabled : true, // is it enabled?
  515.                 active    : false, // is it pressed?
  516.                 text    : btn[2], // enabled in text mode?
  517.                 cmd    : btn[3], // the command ID
  518.                 state    : setButtonStatus, // for changing state
  519.                 context : btn[4] || null // enabled in a certain context?
  520.             };
  521.             tb_objects[txt] = obj;
  522.             // handlers to emulate nice flat toolbar buttons
  523.             HTMLArea._addEvent(el, "mouseover", function () {
  524.                 if (obj.enabled) {
  525.                     HTMLArea._addClass(el, "buttonHover");
  526.                 }
  527.             });
  528.             HTMLArea._addEvent(el, "mouseout", function () {
  529.                 if (obj.enabled) with (HTMLArea) {
  530.                     _removeClass(el, "buttonHover");
  531.                     _removeClass(el, "buttonActive");
  532.                     (obj.active) && _addClass(el, "buttonPressed");
  533.                 }
  534.             });
  535.             HTMLArea._addEvent(el, "mousedown", function (ev) {
  536.                 if (obj.enabled) with (HTMLArea) {
  537.                     _addClass(el, "buttonActive");
  538.                     _removeClass(el, "buttonPressed");
  539.                     _stopEvent(is_ie ? window.event : ev);
  540.                 }
  541.             });
  542.             // when clicked, do the following:
  543.             HTMLArea._addEvent(el, "click", function (ev) {
  544.                 if (obj.enabled) with (HTMLArea) {
  545.                     _removeClass(el, "buttonActive");
  546.                     _removeClass(el, "buttonHover");
  547.                     obj.cmd(editor, obj.name, obj);
  548.                     _stopEvent(is_ie ? window.event : ev);
  549.                 }
  550.             });
  551.             var img = document.createElement("img");
  552.             img.src = btn[1];
  553.             img.style.width = "18px";
  554.             img.style.height = "18px";
  555.             el.appendChild(img);
  556.         } else if (!el) {
  557.             el = createSelect(txt);
  558.         }
  559.         if (el) {
  560.             var tb_cell = document.createElement("td");
  561.             tb_row.appendChild(tb_cell);
  562.             tb_cell.appendChild(el);
  563.         } else {
  564.             alert("FIXME: Unknown toolbar item: " + txt);
  565.         }
  566.         return el;
  567.     };
  568.  
  569.     var first = true;
  570.     for (var i in this.config.toolbar) {
  571.         if (!first) {
  572.             createButton("linebreak");
  573.         } else {
  574.             first = false;
  575.         }
  576.         var group = this.config.toolbar[i];
  577.         for (var j in group) {
  578.             var code = group[j];
  579.             if (/^([IT])\[(.*?)\]/.test(code)) {
  580.                 // special case, create text label
  581.                 var l7ed = RegExp.$1 == "I"; // localized?
  582.                 var label = RegExp.$2;
  583.                 if (l7ed) {
  584.                     label = HTMLArea.I18N.custom[label];
  585.                 }
  586.                 var tb_cell = document.createElement("td");
  587.                 tb_row.appendChild(tb_cell);
  588.                 tb_cell.className = "label";
  589.                 tb_cell.innerHTML = label;
  590.             } else {
  591.                 createButton(code);
  592.             }
  593.         }
  594.     }
  595.  
  596.     this._htmlArea.appendChild(toolbar);
  597. };
  598.  
  599. HTMLArea.prototype._createStatusBar = function() {
  600.     var statusbar = document.createElement("div");
  601.     statusbar.className = "statusBar";
  602.     this._htmlArea.appendChild(statusbar);
  603.     this._statusBar = statusbar;
  604.     // statusbar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
  605.     // creates a holder for the path view
  606.     div = document.createElement("span");
  607.     div.className = "statusBarTree";
  608.     div.innerHTML = HTMLArea.I18N.msg["Path"] + ": ";
  609.     this._statusBarTree = div;
  610.     this._statusBar.appendChild(div);
  611.     if (!this.config.statusBar) {
  612.         // disable it...
  613.         statusbar.style.display = "none";
  614.     }
  615. };
  616.  
  617. // Creates the HTMLArea object and replaces the textarea with it.
  618. HTMLArea.prototype.generate = function () {
  619.     var editor = this;    // we'll need "this" in some nested functions
  620.     // get the textarea
  621.     var textarea = this._textArea;
  622.     if (typeof textarea == "string") {
  623.         // it's not element but ID
  624.         this._textArea = textarea = HTMLArea.getElementById("textarea", textarea);
  625.     }
  626.     this._ta_size = {
  627.         w: textarea.offsetWidth,
  628.         h: textarea.offsetHeight
  629.     };
  630.     textarea.style.display = "none";
  631.  
  632.     // create the editor framework
  633.     var htmlarea = document.createElement("div");
  634.     htmlarea.className = "htmlarea";
  635.     this._htmlArea = htmlarea;
  636.  
  637.     // insert the editor before the textarea.
  638.     textarea.parentNode.insertBefore(htmlarea, textarea);
  639.  
  640.     if (textarea.form) {
  641.         // we have a form, on submit get the HTMLArea content and
  642.         // update original textarea.
  643.         var f = textarea.form;
  644.         if (typeof f.onsubmit == "function") {
  645.             var funcref = f.onsubmit;
  646.             if (typeof f.__msh_prevOnSubmit == "undefined") {
  647.                 f.__msh_prevOnSubmit = [];
  648.             }
  649.             f.__msh_prevOnSubmit.push(funcref);
  650.         }
  651.         f.onsubmit = function() {
  652.             editor._textArea.value = editor.getHTML();
  653.             var a = this.__msh_prevOnSubmit;
  654.             // call previous submit methods if they were there.
  655.             if (typeof a != "undefined") {
  656.                 for (var i in a) {
  657.                     a[i]();
  658.                 }
  659.             }
  660.         };
  661.     }
  662.  
  663.     // add a handler for the "back/forward" case -- on body.unload we save
  664.     // the HTML content into the original textarea.
  665.     try {
  666.         window.onunload = function() {
  667.             editor._textArea.value = editor.getHTML();
  668.         };
  669.     } catch(e) {};
  670.  
  671.     // creates & appends the toolbar
  672.     this._createToolbar();
  673.  
  674.     // create the IFRAME
  675.     var iframe = document.createElement("iframe");
  676.     htmlarea.appendChild(iframe);
  677.  
  678.     this._iframe = iframe;
  679.  
  680.     // creates & appends the status bar, if the case
  681.     this._createStatusBar();
  682.  
  683.     // remove the default border as it keeps us from computing correctly
  684.     // the sizes.  (somebody tell me why doesn't this work in IE)
  685.  
  686.     if (!HTMLArea.is_ie) {
  687.         iframe.style.borderWidth = "1px";
  688.     // iframe.frameBorder = "1";
  689.     // iframe.marginHeight = "0";
  690.     // iframe.marginWidth = "0";
  691.     }
  692.  
  693.     // size the IFRAME according to user's prefs or initial textarea
  694.     var height = (this.config.height == "auto" ? (this._ta_size.h + "px") : this.config.height);
  695.     height = parseInt(height);
  696.     var width = (this.config.width == "auto" ? (this._ta_size.w + "px") : this.config.width);
  697.     width = parseInt(width);
  698.  
  699.     if (!HTMLArea.is_ie) {
  700.         height -= 2;
  701.         width -= 2;
  702.     }
  703.  
  704.     iframe.style.width = width + "px";
  705.     if (this.config.sizeIncludesToolbar) {
  706.         // substract toolbar height
  707.         height -= this._toolbar.offsetHeight;
  708.         height -= this._statusBar.offsetHeight;
  709.     }
  710.     if (height < 0) {
  711.         height = 0;
  712.     }
  713.     iframe.style.height = height + "px";
  714.  
  715.     // the editor including the toolbar now have the same size as the
  716.     // original textarea.. which means that we need to reduce that a bit.
  717.     textarea.style.width = iframe.style.width;
  718.      textarea.style.height = iframe.style.height;
  719.  
  720.     // IMPORTANT: we have to allow Mozilla a short time to recognize the
  721.     // new frame.  Otherwise we get a stupid exception.
  722.     function initIframe() {
  723.         var doc = editor._iframe.contentWindow.document;
  724.         if (!doc) {
  725.             // Try again..
  726.             // FIXME: don't know what else to do here.  Normally
  727.             // we'll never reach this point.
  728.             if (HTMLArea.is_gecko) {
  729.                 setTimeout(initIframe, 100);
  730.                 return false;
  731.             } else {
  732.                 alert("ERROR: IFRAME can't be initialized.");
  733.             }
  734.         }
  735.         if (HTMLArea.is_gecko) {
  736.             // enable editable mode for Mozilla
  737.             doc.designMode = "on";
  738.         }
  739.         editor._doc = doc;
  740.         if (!editor.config.fullPage) {
  741.             doc.open();
  742.             var html = "<html>\n";
  743.             html += "<head>\n";
  744.             if (editor.config.baseURL)
  745.                 html += '<base href="' + editor.config.baseURL + '" />';
  746.             html += "<style>" + editor.config.pageStyle +
  747.                 " html,body { border: 0px; }</style>\n";
  748.             html += "</head>\n";
  749.             html += "<body>\n";
  750.             html += editor._textArea.value;
  751.             html += "</body>\n";
  752.             html += "</html>";
  753.             doc.write(html);
  754.             doc.close();
  755.         } else {
  756.             var html = editor._textArea.value;
  757.             if (html.match(HTMLArea.RE_doctype)) {
  758.                 editor.setDoctype(RegExp.$1);
  759.                 html = html.replace(HTMLArea.RE_doctype, "");
  760.             }
  761.             doc.open();
  762.             doc.write(html);
  763.             doc.close();
  764.         }
  765.  
  766.         if (HTMLArea.is_ie) {
  767.             // enable editable mode for IE.     For some reason this
  768.             // doesn't work if done in the same place as for Gecko
  769.             // (above).
  770.             doc.body.contentEditable = true;
  771.         }
  772.  
  773.         editor.focusEditor();
  774.         // intercept some events; for updating the toolbar & keyboard handlers
  775.         HTMLArea._addEvents
  776.             (doc, ["keydown", "keypress", "mousedown", "mouseup", "drag"],
  777.              function (event) {
  778.                  return editor._editorEvent(HTMLArea.is_ie ? editor._iframe.contentWindow.event : event);
  779.              });
  780.  
  781.         // check if any plugins have registered refresh handlers
  782.         for (var i in editor.plugins) {
  783.             var plugin = editor.plugins[i].instance;
  784.             if (typeof plugin.onGenerate == "function")
  785.                 plugin.onGenerate();
  786.         }
  787.  
  788.         setTimeout(function() {
  789.             editor.updateToolbar();
  790.         }, 250);
  791.  
  792.         if (typeof editor.onGenerate == "function")
  793.             editor.onGenerate();
  794.     };
  795.     setTimeout(initIframe, 100);
  796. };
  797.  
  798. // Switches editor mode; parameter can be "textmode" or "wysiwyg".  If no
  799. // parameter was passed this function toggles between modes.
  800. HTMLArea.prototype.setMode = function(mode) {
  801.     if (typeof mode == "undefined") {
  802.         mode = ((this._editMode == "textmode") ? "wysiwyg" : "textmode");
  803.     }
  804.     switch (mode) {
  805.         case "textmode":
  806.         this._textArea.value = this.getHTML();
  807.         this._iframe.style.display = "none";
  808.         this._textArea.style.display = "block";
  809.         if (this.config.statusBar) {
  810.             this._statusBar.innerHTML = HTMLArea.I18N.msg["TEXT_MODE"];
  811.         }
  812.         break;
  813.         case "wysiwyg":
  814.         if (HTMLArea.is_gecko) {
  815.             // disable design mode before changing innerHTML
  816.             try {
  817.                 this._doc.designMode = "off";
  818.             } catch(e) {};
  819.         }
  820.         if (!this.config.fullPage)
  821.             this._doc.body.innerHTML = this.getHTML();
  822.         else
  823.             this.setFullHTML(this.getHTML());
  824.         this._iframe.style.display = "block";
  825.         this._textArea.style.display = "none";
  826.         if (HTMLArea.is_gecko) {
  827.             // we need to refresh that info for Moz-1.3a
  828.             try {
  829.                 this._doc.designMode = "on";
  830.             } catch(e) {};
  831.         }
  832.         if (this.config.statusBar) {
  833.             this._statusBar.innerHTML = '';
  834.             this._statusBar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
  835.             this._statusBar.appendChild(this._statusBarTree);
  836.         }
  837.         break;
  838.         default:
  839.         alert("Mode <" + mode + "> not defined!");
  840.         return false;
  841.     }
  842.     this._editMode = mode;
  843.     this.focusEditor();
  844. };
  845.  
  846. HTMLArea.prototype.setFullHTML = function(html) {
  847.     var save_multiline = RegExp.multiline;
  848.     RegExp.multiline = true;
  849.     if (html.match(HTMLArea.RE_doctype)) {
  850.         this.setDoctype(RegExp.$1);
  851.         html = html.replace(HTMLArea.RE_doctype, "");
  852.     }
  853.     RegExp.multiline = save_multiline;
  854.     if (!HTMLArea.is_ie) {
  855.         if (html.match(HTMLArea.RE_head))
  856.             this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1;
  857.         if (html.match(HTMLArea.RE_body))
  858.             this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1;
  859.     } else {
  860.         var html_re = /<html>((.|\n)*?)<\/html>/i;
  861.         html = html.replace(html_re, "$1");
  862.         this._doc.open();
  863.         this._doc.write(html);
  864.         this._doc.close();
  865.         this._doc.body.contentEditable = true;
  866.         return true;
  867.     }
  868. };
  869.  
  870. /***************************************************
  871.  *  Category: PLUGINS
  872.  ***************************************************/
  873.  
  874. // this is the variant of the function above where the plugin arguments are
  875. // already packed in an array.  Externally, it should be only used in the
  876. // full-screen editor code, in order to initialize plugins with the same
  877. // parameters as in the opener window.
  878. HTMLArea.prototype.registerPlugin2 = function(plugin, args) {
  879.     if (typeof plugin == "string")
  880.         plugin = eval(plugin);
  881.     if (typeof plugin == "undefined") {
  882.         /* FIXME: This should never happen. But why does it do? */
  883.         return false;
  884.     }
  885.     var obj = new plugin(this, args);
  886.     if (obj) {
  887.         var clone = {};
  888.         var info = plugin._pluginInfo;
  889.         for (var i in info)
  890.             clone[i] = info[i];
  891.         clone.instance = obj;
  892.         clone.args = args;
  893.         this.plugins[plugin._pluginInfo.name] = clone;
  894.     } else
  895.         alert("Can't register plugin " + plugin.toString() + ".");
  896. };
  897.  
  898. // Create the specified plugin and register it with this HTMLArea
  899. HTMLArea.prototype.registerPlugin = function() {
  900.     var plugin = arguments[0];
  901.     var args = [];
  902.     for (var i = 1; i < arguments.length; ++i)
  903.         args.push(arguments[i]);
  904.     this.registerPlugin2(plugin, args);
  905. };
  906.  
  907. // static function that loads the required plugin and lang file, based on the
  908. // language loaded already for HTMLArea.  You better make sure that the plugin
  909. // _has_ that language, otherwise shit might happen ;-)
  910. HTMLArea.loadPlugin = function(pluginName) {
  911.     var dir = _editor_url + "plugins/" + pluginName;
  912.     var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g,
  913.                     function (str, l1, l2, l3) {
  914.                         return l1 + "-" + l2.toLowerCase() + l3;
  915.                     }).toLowerCase() + ".js";
  916.     var plugin_file = dir + "/" + plugin;
  917.     var plugin_lang = dir + "/lang/" + HTMLArea.I18N.lang + ".js";
  918.     HTMLArea._scripts.push(plugin_file, plugin_lang);
  919.     document.write("<script type='text/javascript' src='" + plugin_file + "'></script>");
  920.     document.write("<script type='text/javascript' src='" + plugin_lang + "'></script>");
  921. };
  922.  
  923. HTMLArea.loadStyle = function(style, plugin) {
  924.     var url = _editor_url || '';
  925.     if (typeof plugin != "undefined") {
  926.         url += "plugins/" + plugin + "/";
  927.     }
  928.     url += style;
  929.     document.write("<style type='text/css'>@import url(" + url + ");</style>");
  930. };
  931. HTMLArea.loadStyle("htmlarea.css");
  932.  
  933. /***************************************************
  934.  *  Category: EDITOR UTILITIES
  935.  ***************************************************/
  936.  
  937. // The following function is a slight variation of the word cleaner code posted
  938. // by Weeezl (user @ InteractiveTools forums).
  939. HTMLArea.prototype._wordClean = function() {
  940.     var D = this.getInnerHTML();
  941.     if (D.indexOf('class=Mso') >= 0) {
  942.  
  943.         // make one line
  944.         D = D.replace(/\r\n/g, ' ').
  945.             replace(/\n/g, ' ').
  946.             replace(/\r/g, ' ').
  947.             replace(/\ \;/g,' ');
  948.  
  949.         // keep tags, strip attributes
  950.         D = D.replace(/ class=[^\s|>]*/gi,'').
  951.             //replace(/<p [^>]*TEXT-ALIGN: justify[^>]*>/gi,'<p align="justify">').
  952.             replace(/ style=\"[^>]*\"/gi,'').
  953.             replace(/ align=[^\s|>]*/gi,'');
  954.  
  955.         //clean up tags
  956.         D = D.replace(/<b [^>]*>/gi,'<b>').
  957.             replace(/<i [^>]*>/gi,'<i>').
  958.             replace(/<li [^>]*>/gi,'<li>').
  959.             replace(/<ul [^>]*>/gi,'<ul>');
  960.  
  961.         // replace outdated tags
  962.         D = D.replace(/<b>/gi,'<strong>').
  963.             replace(/<\/b>/gi,'</strong>');
  964.  
  965.         // mozilla doesn't like <em> tags
  966.         D = D.replace(/<em>/gi,'<i>').
  967.             replace(/<\/em>/gi,'</i>');
  968.  
  969.         // kill unwanted tags
  970.         D = D.replace(/<\?xml:[^>]*>/g, '').       // Word xml
  971.             replace(/<\/?st1:[^>]*>/g,'').     // Word SmartTags
  972.             replace(/<\/?[a-z]\:[^>]*>/g,'').  // All other funny Word non-HTML stuff
  973.             replace(/<\/?font[^>]*>/gi,'').    // Disable if you want to keep font formatting
  974.             replace(/<\/?span[^>]*>/gi,' ').
  975.             replace(/<\/?div[^>]*>/gi,' ').
  976.             replace(/<\/?pre[^>]*>/gi,' ').
  977.             replace(/<\/?h[1-6][^>]*>/gi,' ');
  978.  
  979.         //remove empty tags
  980.         //D = D.replace(/<strong><\/strong>/gi,'').
  981.         //replace(/<i><\/i>/gi,'').
  982.         //replace(/<P[^>]*><\/P>/gi,'');
  983.  
  984.         // nuke double tags
  985.         oldlen = D.length + 1;
  986.         while(oldlen > D.length) {
  987.             oldlen = D.length;
  988.             // join us now and free the tags, we'll be free hackers, we'll be free... ;-)
  989.             D = D.replace(/<([a-z][a-z]*)> *<\/\1>/gi,' ').
  990.                 replace(/<([a-z][a-z]*)> *<([a-z][^>]*)> *<\/\1>/gi,'<$2>');
  991.         }
  992.         D = D.replace(/<([a-z][a-z]*)><\1>/gi,'<$1>').
  993.             replace(/<\/([a-z][a-z]*)><\/\1>/gi,'<\/$1>');
  994.  
  995.         // nuke double spaces
  996.         D = D.replace(/  */gi,' ');
  997.  
  998.         this.setHTML(D);
  999.         this.updateToolbar();
  1000.     }
  1001. };
  1002.  
  1003. HTMLArea.prototype.forceRedraw = function() {
  1004.     this._doc.body.style.visibility = "hidden";
  1005.     this._doc.body.style.visibility = "visible";
  1006.     // this._doc.body.innerHTML = this.getInnerHTML();
  1007. };
  1008.  
  1009. // focuses the iframe window.  returns a reference to the editor document.
  1010. HTMLArea.prototype.focusEditor = function() {
  1011.     switch (this._editMode) {
  1012.         // notice the try { ... } catch block to avoid some rare exceptions in FireFox
  1013.         // (perhaps also in other Gecko browsers). Manual focus by user is required in
  1014.         // case of an error. Somebody has an idea?
  1015.         case "wysiwyg" : try { this._iframe.contentWindow.focus() } catch (e) {} break;
  1016.         case "textmode": try { this._textArea.focus() } catch (e) {} break;
  1017.         default       : alert("ERROR: mode " + this._editMode + " is not defined");
  1018.     }
  1019.     return this._doc;
  1020. };
  1021.  
  1022. // takes a snapshot of the current text (for undo)
  1023. HTMLArea.prototype._undoTakeSnapshot = function() {
  1024.     ++this._undoPos;
  1025.     if (this._undoPos >= this.config.undoSteps) {
  1026.         // remove the first element
  1027.         this._undoQueue.shift();
  1028.         --this._undoPos;
  1029.     }
  1030.     // use the fasted method (getInnerHTML);
  1031.     var take = true;
  1032.     var txt = this.getInnerHTML();
  1033.     if (this._undoPos > 0)
  1034.         take = (this._undoQueue[this._undoPos - 1] != txt);
  1035.     if (take) {
  1036.         this._undoQueue[this._undoPos] = txt;
  1037.     } else {
  1038.         this._undoPos--;
  1039.     }
  1040. };
  1041.  
  1042. HTMLArea.prototype.undo = function() {
  1043.     if (this._undoPos > 0) {
  1044.         var txt = this._undoQueue[--this._undoPos];
  1045.         if (txt) this.setHTML(txt);
  1046.         else ++this._undoPos;
  1047.     }
  1048. };
  1049.  
  1050. HTMLArea.prototype.redo = function() {
  1051.     if (this._undoPos < this._undoQueue.length - 1) {
  1052.         var txt = this._undoQueue[++this._undoPos];
  1053.         if (txt) this.setHTML(txt);
  1054.         else --this._undoPos;
  1055.     }
  1056. };
  1057.  
  1058. // updates enabled/disable/active state of the toolbar elements
  1059. HTMLArea.prototype.updateToolbar = function(noStatus) {
  1060.     var doc = this._doc;
  1061.     var text = (this._editMode == "textmode");
  1062.     var ancestors = null;
  1063.     if (!text) {
  1064.         ancestors = this.getAllAncestors();
  1065.         if (this.config.statusBar && !noStatus) {
  1066.             this._statusBarTree.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; // clear
  1067.             for (var i = ancestors.length; --i >= 0;) {
  1068.                 var el = ancestors[i];
  1069.                 if (!el) {
  1070.                     // hell knows why we get here; this
  1071.                     // could be a classic example of why
  1072.                     // it's good to check for conditions
  1073.                     // that are impossible to happen ;-)
  1074.                     continue;
  1075.                 }
  1076.                 var a = document.createElement("a");
  1077.                 a.href = "#";
  1078.                 a.el = el;
  1079.                 a.editor = this;
  1080.                 a.onclick = function() {
  1081.                     this.blur();
  1082.                     this.editor.selectNodeContents(this.el);
  1083.                     this.editor.updateToolbar(true);
  1084.                     return false;
  1085.                 };
  1086.                 a.oncontextmenu = function() {
  1087.                     // TODO: add context menu here
  1088.                     this.blur();
  1089.                     var info = "Inline style:\n\n";
  1090.                     info += this.el.style.cssText.split(/;\s*/).join(";\n");
  1091.                     alert(info);
  1092.                     return false;
  1093.                 };
  1094.                 var txt = el.tagName.toLowerCase();
  1095.                 a.title = el.style.cssText;
  1096.                 if (el.id) {
  1097.                     txt += "#" + el.id;
  1098.                 }
  1099.                 if (el.className) {
  1100.                     txt += "." + el.className;
  1101.                 }
  1102.                 a.appendChild(document.createTextNode(txt));
  1103.                 this._statusBarTree.appendChild(a);
  1104.                 if (i != 0) {
  1105.                     this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb)));
  1106.                 }
  1107.             }
  1108.         }
  1109.     }
  1110.  
  1111.     for (var i in this._toolbarObjects) {
  1112.         var btn = this._toolbarObjects[i];
  1113.         var cmd = i;
  1114.         var inContext = true;
  1115.         if (btn.context && !text) {
  1116.             inContext = false;
  1117.             var context = btn.context;
  1118.             var attrs = [];
  1119.             if (/(.*)\[(.*?)\]/.test(context)) {
  1120.                 context = RegExp.$1;
  1121.                 attrs = RegExp.$2.split(",");
  1122.             }
  1123.             context = context.toLowerCase();
  1124.             var match = (context == "*");
  1125.             for (var k in ancestors) {
  1126.                 if (!ancestors[k]) {
  1127.                     // the impossible really happens.
  1128.                     continue;
  1129.                 }
  1130.                 if (match || (ancestors[k].tagName.toLowerCase() == context)) {
  1131.                     inContext = true;
  1132.                     for (var ka in attrs) {
  1133.                         if (!eval("ancestors[k]." + attrs[ka])) {
  1134.                             inContext = false;
  1135.                             break;
  1136.                         }
  1137.                     }
  1138.                     if (inContext) {
  1139.                         break;
  1140.                     }
  1141.                 }
  1142.             }
  1143.         }
  1144.         btn.state("enabled", (!text || btn.text) && inContext);
  1145.         if (typeof cmd == "function") {
  1146.             continue;
  1147.         }
  1148.         // look-it-up in the custom dropdown boxes
  1149.         var dropdown = this.config.customSelects[cmd];
  1150.         if ((!text || btn.text) && (typeof dropdown != "undefined")) {
  1151.             dropdown.refresh(this);
  1152.             continue;
  1153.         }
  1154.         switch (cmd) {
  1155.             case "fontname":
  1156.             case "fontsize":
  1157.             case "formatblock":
  1158.             if (!text) try {
  1159.                 var value = ("" + doc.queryCommandValue(cmd)).toLowerCase();
  1160.                 if (!value) {
  1161.                     // FIXME: what do we do here?
  1162.                     break;
  1163.                 }
  1164.                 // HACK -- retrieve the config option for this
  1165.                 // combo box.  We rely on the fact that the
  1166.                 // variable in config has the same name as
  1167.                 // button name in the toolbar.
  1168.                 var options = this.config[cmd];
  1169.                 var k = 0;
  1170.                 // btn.element.selectedIndex = 0;
  1171.                 for (var j in options) {
  1172.                     // FIXME: the following line is scary.
  1173.                     if ((j.toLowerCase() == value) ||
  1174.                         (options[j].substr(0, value.length).toLowerCase() == value)) {
  1175.                         btn.element.selectedIndex = k;
  1176.                         break;
  1177.                     }
  1178.                     ++k;
  1179.                 }
  1180.             } catch(e) {};
  1181.             break;
  1182.             case "textindicator":
  1183.             if (!text) {
  1184.                 try {with (btn.element.style) {
  1185.                     backgroundColor = HTMLArea._makeColor(
  1186.                         doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor"));
  1187.                     if (/transparent/i.test(backgroundColor)) {
  1188.                         // Mozilla
  1189.                         backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor"));
  1190.                     }
  1191.                     color = HTMLArea._makeColor(doc.queryCommandValue("forecolor"));
  1192.                     fontFamily = doc.queryCommandValue("fontname");
  1193.                     fontWeight = doc.queryCommandState("bold") ? "bold" : "normal";
  1194.                     fontStyle = doc.queryCommandState("italic") ? "italic" : "normal";
  1195.                 }} catch (e) {
  1196.                     // alert(e + "\n\n" + cmd);
  1197.                 }
  1198.             }
  1199.             break;
  1200.             case "htmlmode": btn.state("active", text); break;
  1201.             case "lefttoright":
  1202.             case "righttoleft":
  1203.             var el = this.getParentElement();
  1204.             while (el && !HTMLArea.isBlockElement(el))
  1205.                 el = el.parentNode;
  1206.             if (el)
  1207.                 btn.state("active", (el.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr")));
  1208.             break;
  1209.             default:
  1210.             try {
  1211.                 btn.state("active", (!text && doc.queryCommandState(cmd)));
  1212.             } catch (e) {}
  1213.         }
  1214.     }
  1215.     // take undo snapshots
  1216.     if (this._customUndo && !this._timerUndo) {
  1217.         this._undoTakeSnapshot();
  1218.         var editor = this;
  1219.         this._timerUndo = setTimeout(function() {
  1220.             editor._timerUndo = null;
  1221.         }, this.config.undoTimeout);
  1222.     }
  1223.  
  1224.     // check if any plugins have registered refresh handlers
  1225.     for (var i in this.plugins) {
  1226.         var plugin = this.plugins[i].instance;
  1227.         if (typeof plugin.onUpdateToolbar == "function")
  1228.             plugin.onUpdateToolbar();
  1229.     }
  1230. };
  1231.  
  1232. /** Returns a node after which we can insert other nodes, in the current
  1233.  * selection.  The selection is removed.  It splits a text node, if needed.
  1234.  */
  1235. HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) {
  1236.     if (!HTMLArea.is_ie) {
  1237.         var sel = this._getSelection();
  1238.         var range = this._createRange(sel);
  1239.         // remove the current selection
  1240.         sel.removeAllRanges();
  1241.         range.deleteContents();
  1242.         var node = range.startContainer;
  1243.         var pos = range.startOffset;
  1244.         switch (node.nodeType) {
  1245.             case 3: // Node.TEXT_NODE
  1246.             // we have to split it at the caret position.
  1247.             if (toBeInserted.nodeType == 3) {
  1248.                 // do optimized insertion
  1249.                 node.insertData(pos, toBeInserted.data);
  1250.                 range = this._createRange();
  1251.                 range.setEnd(node, pos + toBeInserted.length);
  1252.                 range.setStart(node, pos + toBeInserted.length);
  1253.                 sel.addRange(range);
  1254.             } else {
  1255.                 node = node.splitText(pos);
  1256.                 var selnode = toBeInserted;
  1257.                 if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
  1258.                     selnode = selnode.firstChild;
  1259.                 }
  1260.                 node.parentNode.insertBefore(toBeInserted, node);
  1261.                 this.selectNodeContents(selnode);
  1262.                 this.updateToolbar();
  1263.             }
  1264.             break;
  1265.             case 1: // Node.ELEMENT_NODE
  1266.             var selnode = toBeInserted;
  1267.             if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
  1268.                 selnode = selnode.firstChild;
  1269.             }
  1270.             node.insertBefore(toBeInserted, node.childNodes[pos]);
  1271.             this.selectNodeContents(selnode);
  1272.             this.updateToolbar();
  1273.             break;
  1274.         }
  1275.     } else {
  1276.         return null;    // this function not yet used for IE <FIXME>
  1277.     }
  1278. };
  1279.  
  1280. // Returns the deepest node that contains both endpoints of the selection.
  1281. HTMLArea.prototype.getParentElement = function() {
  1282.     var sel = this._getSelection();
  1283.     var range = this._createRange(sel);
  1284.     if (HTMLArea.is_ie) {
  1285.         switch (sel.type) {
  1286.             case "Text":
  1287.             case "None":
  1288.             // It seems that even for selection of type "None",
  1289.             // there _is_ a parent element and it's value is not
  1290.             // only correct, but very important to us.  MSIE is
  1291.             // certainly the buggiest browser in the world and I
  1292.             // wonder, God, how can Earth stand it?
  1293.             return range.parentElement();
  1294.             case "Control":
  1295.             return range.item(0);
  1296.             default:
  1297.             return this._doc.body;
  1298.         }
  1299.     } else try {
  1300.         var p = range.commonAncestorContainer;
  1301.         if (!range.collapsed && range.startContainer == range.endContainer &&
  1302.             range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes())
  1303.             p = range.startContainer.childNodes[range.startOffset];
  1304.         /*
  1305.         alert(range.startContainer + ":" + range.startOffset + "\n" +
  1306.               range.endContainer + ":" + range.endOffset);
  1307.         */
  1308.         while (p.nodeType == 3) {
  1309.             p = p.parentNode;
  1310.         }
  1311.         return p;
  1312.     } catch (e) {
  1313.         return null;
  1314.     }
  1315. };
  1316.  
  1317. // Returns an array with all the ancestor nodes of the selection.
  1318. HTMLArea.prototype.getAllAncestors = function() {
  1319.     var p = this.getParentElement();
  1320.     var a = [];
  1321.     while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
  1322.         a.push(p);
  1323.         p = p.parentNode;
  1324.     }
  1325.     a.push(this._doc.body);
  1326.     return a;
  1327. };
  1328.  
  1329. // Selects the contents inside the given node
  1330. HTMLArea.prototype.selectNodeContents = function(node, pos) {
  1331.     this.focusEditor();
  1332.     this.forceRedraw();
  1333.     var range;
  1334.     var collapsed = (typeof pos != "undefined");
  1335.     if (HTMLArea.is_ie) {
  1336.         range = this._doc.body.createTextRange();
  1337.         range.moveToElementText(node);
  1338.         (collapsed) && range.collapse(pos);
  1339.         range.select();
  1340.     } else {
  1341.         var sel = this._getSelection();
  1342.         range = this._doc.createRange();
  1343.         range.selectNodeContents(node);
  1344.         (collapsed) && range.collapse(pos);
  1345.         sel.removeAllRanges();
  1346.         sel.addRange(range);
  1347.     }
  1348. };
  1349.  
  1350. /** Call this function to insert HTML code at the current position.  It deletes
  1351.  * the selection, if any.
  1352.  */
  1353. HTMLArea.prototype.insertHTML = function(html) {
  1354.     var sel = this._getSelection();
  1355.     var range = this._createRange(sel);
  1356.     if (HTMLArea.is_ie) {
  1357.         range.pasteHTML(html);
  1358.     } else {
  1359.         // construct a new document fragment with the given HTML
  1360.         var fragment = this._doc.createDocumentFragment();
  1361.         var div = this._doc.createElement("div");
  1362.         div.innerHTML = html;
  1363.         while (div.firstChild) {
  1364.             // the following call also removes the node from div
  1365.             fragment.appendChild(div.firstChild);
  1366.         }
  1367.         // this also removes the selection
  1368.         var node = this.insertNodeAtSelection(fragment);
  1369.     }
  1370. };
  1371.  
  1372. /**
  1373.  *  Call this function to surround the existing HTML code in the selection with
  1374.  *  your tags.  FIXME: buggy!  This function will be deprecated "soon".
  1375.  */
  1376. HTMLArea.prototype.surroundHTML = function(startTag, endTag) {
  1377.     var html = this.getSelectedHTML();
  1378.     // the following also deletes the selection
  1379.     this.insertHTML(startTag + html + endTag);
  1380. };
  1381.  
  1382. /// Retrieve the selected block
  1383. HTMLArea.prototype.getSelectedHTML = function() {
  1384.     var sel = this._getSelection();
  1385.     var range = this._createRange(sel);
  1386.     var existing = null;
  1387.     if (HTMLArea.is_ie) {
  1388.         existing = range.htmlText;
  1389.     } else {
  1390.         existing = HTMLArea.getHTML(range.cloneContents(), false, this);
  1391.     }
  1392.     return existing;
  1393. };
  1394.  
  1395. /// Return true if we have some selection
  1396. HTMLArea.prototype.hasSelectedText = function() {
  1397.     // FIXME: come _on_ mishoo, you can do better than this ;-)
  1398.     return this.getSelectedHTML() != '';
  1399. };
  1400.  
  1401. HTMLArea.prototype._createLink = function(link) {
  1402.     var editor = this;
  1403.     var outparam = null;
  1404.     if (typeof link == "undefined") {
  1405.         link = this.getParentElement();
  1406.         if (link && !/^a$/i.test(link.tagName))
  1407.             link = null;
  1408.     }
  1409.     if (link) outparam = {
  1410.         f_href   : HTMLArea.is_ie ? editor.stripBaseURL(link.href) : link.getAttribute("href"),
  1411.         f_title  : link.title,
  1412.         f_target : link.target
  1413.     };
  1414.     this._popupDialog("link.html", function(param) {
  1415.         if (!param)
  1416.             return false;
  1417.         var a = link;
  1418.         if (!a) try {
  1419.             editor._doc.execCommand("createlink", false, param.f_href);
  1420.             a = editor.getParentElement();
  1421.             var sel = editor._getSelection();
  1422.             var range = editor._createRange(sel);
  1423.             if (!HTMLArea.is_ie) {
  1424.                 a = range.startContainer;
  1425.                 if (!/^a$/i.test(a.tagName)) {
  1426.                     a = a.nextSibling;
  1427.                     if (a == null)
  1428.                         a = range.startContainer.parentNode;
  1429.                 }
  1430.             }
  1431.         } catch(e) {}
  1432.         else {
  1433.             var href = param.f_href.trim();
  1434.             editor.selectNodeContents(a);
  1435.             if (href == "") {
  1436.                 editor._doc.execCommand("unlink", false, null);
  1437.                 editor.updateToolbar();
  1438.                 return false;
  1439.             }
  1440.             else {
  1441.                 a.href = href;
  1442.             }
  1443.         }
  1444.         if (!(a && /^a$/i.test(a.tagName)))
  1445.             return false;
  1446.         a.target = param.f_target.trim();
  1447.         a.title = param.f_title.trim();
  1448.         editor.selectNodeContents(a);
  1449.         editor.updateToolbar();
  1450.     }, outparam);
  1451. };
  1452.  
  1453. // Called when the user clicks on "InsertImage" button.  If an image is already
  1454. // there, it will just modify it's properties.
  1455. HTMLArea.prototype._insertImage = function(image) {
  1456.     var editor = this;    // for nested functions
  1457.     var outparam = null;
  1458.     if (typeof image == "undefined") {
  1459.         image = this.getParentElement();
  1460.         if (image && !/^img$/i.test(image.tagName))
  1461.             image = null;
  1462.     }
  1463.     if (image) outparam = {
  1464.         f_url    : HTMLArea.is_ie ? editor.stripBaseURL(image.src) : image.getAttribute("src"),
  1465.         f_alt    : image.alt,
  1466.         f_border : image.border,
  1467.         f_align  : image.align,
  1468.         f_vert   : image.vspace,
  1469.         f_horiz  : image.hspace
  1470.     };
  1471.     this._popupDialog("insert_image.html", function(param) {
  1472.         if (!param) {    // user must have pressed Cancel
  1473.             return false;
  1474.         }
  1475.         var img = image;
  1476.         if (!img) {
  1477.             var sel = editor._getSelection();
  1478.             var range = editor._createRange(sel);
  1479.             editor._doc.execCommand("insertimage", false, param.f_url);
  1480.             if (HTMLArea.is_ie) {
  1481.                 img = range.parentElement();
  1482.                 // wonder if this works...
  1483.                 if (img.tagName.toLowerCase() != "img") {
  1484.                     img = img.previousSibling;
  1485.                 }
  1486.             } else {
  1487.                 img = range.startContainer.previousSibling;
  1488.             }
  1489.         } else {
  1490.             img.src = param.f_url;
  1491.         }
  1492.  
  1493.         for (field in param) {
  1494.             var value = param[field];
  1495.             switch (field) {
  1496.                 case "f_alt"    : img.alt     = value; break;
  1497.                 case "f_border" : img.border = parseInt(value || "0"); break;
  1498.                 case "f_align"  : img.align     = value; break;
  1499.                 case "f_vert"   : img.vspace = parseInt(value || "0"); break;
  1500.                 case "f_horiz"  : img.hspace = parseInt(value || "0"); break;
  1501.             }
  1502.         }
  1503.     }, outparam);
  1504. };
  1505.  
  1506. // Called when the user clicks the Insert Table button
  1507. HTMLArea.prototype._insertTable = function() {
  1508.     var sel = this._getSelection();
  1509.     var range = this._createRange(sel);
  1510.     var editor = this;    // for nested functions
  1511.     this._popupDialog("insert_table.html", function(param) {
  1512.         if (!param) {    // user must have pressed Cancel
  1513.             return false;
  1514.         }
  1515.         var doc = editor._doc;
  1516.         // create the table element
  1517.         var table = doc.createElement("table");
  1518.         // assign the given arguments
  1519.  
  1520.         for (var field in param) {
  1521.             var value = param[field];
  1522.             if (!value) {
  1523.                 continue;
  1524.             }
  1525.             switch (field) {
  1526.                 case "f_width"   : table.style.width = value + param["f_unit"]; break;
  1527.                 case "f_align"   : table.align     = value; break;
  1528.                 case "f_border"  : table.border     = parseInt(value); break;
  1529.                 case "f_spacing" : table.cellSpacing = parseInt(value); break;
  1530.                 case "f_padding" : table.cellPadding = parseInt(value); break;
  1531.             }
  1532.         }
  1533.         var tbody = doc.createElement("tbody");
  1534.         table.appendChild(tbody);
  1535.         for (var i = 0; i < param["f_rows"]; ++i) {
  1536.             var tr = doc.createElement("tr");
  1537.             tbody.appendChild(tr);
  1538.             for (var j = 0; j < param["f_cols"]; ++j) {
  1539.                 var td = doc.createElement("td");
  1540.                 tr.appendChild(td);
  1541.                 // Mozilla likes to see something inside the cell.
  1542.                 (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br"));
  1543.             }
  1544.         }
  1545.         if (HTMLArea.is_ie) {
  1546.             range.pasteHTML(table.outerHTML);
  1547.         } else {
  1548.             // insert the table
  1549.             editor.insertNodeAtSelection(table);
  1550.         }
  1551.         return true;
  1552.     }, null);
  1553. };
  1554.  
  1555. /***************************************************
  1556.  *  Category: EVENT HANDLERS
  1557.  ***************************************************/
  1558.  
  1559. // el is reference to the SELECT object
  1560. // txt is the name of the select field, as in config.toolbar
  1561. HTMLArea.prototype._comboSelected = function(el, txt) {
  1562.     this.focusEditor();
  1563.     var value = el.options[el.selectedIndex].value;
  1564.     switch (txt) {
  1565.         case "fontname":
  1566.         case "fontsize": this.execCommand(txt, false, value); break;
  1567.         case "formatblock":
  1568.         (HTMLArea.is_ie) && (value = "<" + value + ">");
  1569.         this.execCommand(txt, false, value);
  1570.         break;
  1571.         default:
  1572.         // try to look it up in the registered dropdowns
  1573.         var dropdown = this.config.customSelects[txt];
  1574.         if (typeof dropdown != "undefined") {
  1575.             dropdown.action(this);
  1576.         } else {
  1577.             alert("FIXME: combo box " + txt + " not implemented");
  1578.         }
  1579.     }
  1580. };
  1581.  
  1582. // the execCommand function (intercepts some commands and replaces them with
  1583. // our own implementation)
  1584. HTMLArea.prototype.execCommand = function(cmdID, UI, param) {
  1585.     var editor = this;    // for nested functions
  1586.     this.focusEditor();
  1587.     cmdID = cmdID.toLowerCase();
  1588.     switch (cmdID) {
  1589.         case "htmlmode" : this.setMode(); break;
  1590.         case "hilitecolor":
  1591.         (HTMLArea.is_ie) && (cmdID = "backcolor");
  1592.         case "forecolor":
  1593.         this._popupDialog("select_color.html", function(color) {
  1594.             if (color) { // selection not canceled
  1595.                 editor._doc.execCommand(cmdID, false, "#" + color);
  1596.             }
  1597.         }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID)));
  1598.         break;
  1599.         case "createlink":
  1600.         this._createLink();
  1601.         break;
  1602.         case "popupeditor":
  1603.         // this object will be passed to the newly opened window
  1604.         HTMLArea._object = this;
  1605.         if (HTMLArea.is_ie) {
  1606.             //if (confirm(HTMLArea.I18N.msg["IE-sucks-full-screen"]))
  1607.             {
  1608.                 window.open(this.popupURL("fullscreen.html"), "ha_fullscreen",
  1609.                         "toolbar=no,location=no,directories=no,status=no,menubar=no," +
  1610.                         "scrollbars=no,resizable=yes,width=640,height=480");
  1611.             }
  1612.         } else {
  1613.             window.open(this.popupURL("fullscreen.html"), "ha_fullscreen",
  1614.                     "toolbar=no,menubar=no,personalbar=no,width=640,height=480," +
  1615.                     "scrollbars=no,resizable=yes");
  1616.         }
  1617.         break;
  1618.         case "undo":
  1619.         case "redo":
  1620.         if (this._customUndo)
  1621.             this[cmdID]();
  1622.         else
  1623.             this._doc.execCommand(cmdID, UI, param);
  1624.         break;
  1625.         case "inserttable": this._insertTable(); break;
  1626.         case "insertimage": this._insertImage(); break;
  1627.         case "about"    : this._popupDialog("about.html", null, this); break;
  1628.         case "showhelp" : window.open(_editor_url + "reference.html", "ha_help"); break;
  1629.  
  1630.         case "killword": this._wordClean(); break;
  1631.  
  1632.         case "cut":
  1633.         case "copy":
  1634.         case "paste":
  1635.         try {
  1636.             if (this.config.killWordOnPaste)
  1637.                 this._wordClean();
  1638.             this._doc.execCommand(cmdID, UI, param);
  1639.         } catch (e) {
  1640.             if (HTMLArea.is_gecko) {
  1641.                 if (typeof HTMLArea.I18N.msg["Moz-Clipboard"] == "undefined") {
  1642.                     HTMLArea.I18N.msg["Moz-Clipboard"] =
  1643.                         "Unprivileged scripts cannot access Cut/Copy/Paste programatically " +
  1644.                         "for security reasons.  Click OK to see a technical note at mozilla.org " +
  1645.                         "which shows you how to allow a script to access the clipboard.\n\n" +
  1646.                         "[FIXME: please translate this message in your language definition file.]";
  1647.                 }
  1648.                 if (confirm(HTMLArea.I18N.msg["Moz-Clipboard"]))
  1649.                     window.open("http://mozilla.org/editor/midasdemo/securityprefs.html");
  1650.             }
  1651.         }
  1652.         break;
  1653.         case "lefttoright":
  1654.         case "righttoleft":
  1655.         var dir = (cmdID == "righttoleft") ? "rtl" : "ltr";
  1656.         var el = this.getParentElement();
  1657.         while (el && !HTMLArea.isBlockElement(el))
  1658.             el = el.parentNode;
  1659.         if (el) {
  1660.             if (el.style.direction == dir)
  1661.                 el.style.direction = "";
  1662.             else
  1663.                 el.style.direction = dir;
  1664.         }
  1665.         break;
  1666.         default: this._doc.execCommand(cmdID, UI, param);
  1667.     }
  1668.     this.updateToolbar();
  1669.     return false;
  1670. };
  1671.  
  1672. /** A generic event handler for things that happen in the IFRAME's document.
  1673.  * This function also handles key bindings. */
  1674. HTMLArea.prototype._editorEvent = function(ev) {
  1675.     var editor = this;
  1676.     var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress");
  1677.  
  1678.     if (keyEvent) {
  1679.         for (var i in editor.plugins) {
  1680.             var plugin = editor.plugins[i].instance;
  1681.             if (typeof plugin.onKeyPress == "function") plugin.onKeyPress(ev);
  1682.         }
  1683.     }
  1684.     if (keyEvent && ev.ctrlKey && !ev.altKey) {
  1685.         var sel = null;
  1686.         var range = null;
  1687.         var key = String.fromCharCode(HTMLArea.is_ie ? ev.keyCode : ev.charCode).toLowerCase();
  1688.         var cmd = null;
  1689.         var value = null;
  1690.         switch (key) {
  1691.             case 'a':
  1692.             if (!HTMLArea.is_ie) {
  1693.                 // KEY select all
  1694.                 sel = this._getSelection();
  1695.                 sel.removeAllRanges();
  1696.                 range = this._createRange();
  1697.                 range.selectNodeContents(this._doc.body);
  1698.                 sel.addRange(range);
  1699.                 HTMLArea._stopEvent(ev);
  1700.             }
  1701.             break;
  1702.  
  1703.             // simple key commands follow
  1704.  
  1705.             case 'b': cmd = "bold"; break;
  1706.             case 'i': cmd = "italic"; break;
  1707.             case 'u': cmd = "underline"; break;
  1708.             case 's': cmd = "strikethrough"; break;
  1709.             case 'l': cmd = "justifyleft"; break;
  1710.             case 'e': cmd = "justifycenter"; break;
  1711.             case 'r': cmd = "justifyright"; break;
  1712.             case 'j': cmd = "justifyfull"; break;
  1713.             case 'z': cmd = "undo"; break;
  1714.             case 'y': cmd = "redo"; break;
  1715.             case 'v': cmd = "paste"; break;
  1716.  
  1717.             case '0': cmd = "killword"; break;
  1718.  
  1719.             // headings
  1720.             case '1':
  1721.             case '2':
  1722.             case '3':
  1723.             case '4':
  1724.             case '5':
  1725.             case '6':
  1726.             cmd = "formatblock";
  1727.             value = "h" + key;
  1728.             if (HTMLArea.is_ie) {
  1729.                 value = "<" + value + ">";
  1730.             }
  1731.             break;
  1732.         }
  1733.         if (cmd) {
  1734.             // execute simple command
  1735.             this.execCommand(cmd, false, value);
  1736.             HTMLArea._stopEvent(ev);
  1737.         }
  1738.     }
  1739.     /*
  1740.     else if (keyEvent) {
  1741.         // other keys here
  1742.         switch (ev.keyCode) {
  1743.             case 13: // KEY enter
  1744.             // if (HTMLArea.is_ie) {
  1745.             this.insertHTML("<br />");
  1746.             HTMLArea._stopEvent(ev);
  1747.             // }
  1748.             break;
  1749.         }
  1750.     }
  1751.     */
  1752.     // update the toolbar state after some time
  1753.     if (editor._timerToolbar) {
  1754.         clearTimeout(editor._timerToolbar);
  1755.     }
  1756.     editor._timerToolbar = setTimeout(function() {
  1757.         editor.updateToolbar();
  1758.         editor._timerToolbar = null;
  1759.     }, 50);
  1760. };
  1761.  
  1762. // retrieve the HTML
  1763. HTMLArea.prototype.getHTML = function() {
  1764.     switch (this._editMode) {
  1765.         case "wysiwyg"  :
  1766.         if (!this.config.fullPage) {
  1767.             return HTMLArea.getHTML(this._doc.body, false, this);
  1768.         } else
  1769.             return this.doctype + "\n" + HTMLArea.getHTML(this._doc.documentElement, true, this);
  1770.         case "textmode" : return this._textArea.value;
  1771.         default        : alert("Mode <" + mode + "> not defined!");
  1772.     }
  1773.     return false;
  1774. };
  1775.  
  1776. // retrieve the HTML (fastest version, but uses innerHTML)
  1777. HTMLArea.prototype.getInnerHTML = function() {
  1778.     switch (this._editMode) {
  1779.         case "wysiwyg"  :
  1780.         if (!this.config.fullPage)
  1781.             return this._doc.body.innerHTML;
  1782.         else
  1783.             return this.doctype + "\n" + this._doc.documentElement.innerHTML;
  1784.         case "textmode" : return this._textArea.value;
  1785.         default        : alert("Mode <" + mode + "> not defined!");
  1786.     }
  1787.     return false;
  1788. };
  1789.  
  1790. // completely change the HTML inside
  1791. HTMLArea.prototype.setHTML = function(html) {
  1792.     switch (this._editMode) {
  1793.         case "wysiwyg"  :
  1794.         if (!this.config.fullPage)
  1795.             this._doc.body.innerHTML = html;
  1796.         else
  1797.             // this._doc.documentElement.innerHTML = html;
  1798.             this._doc.body.innerHTML = html;
  1799.         break;
  1800.         case "textmode" : this._textArea.value = html; break;
  1801.         default        : alert("Mode <" + mode + "> not defined!");
  1802.     }
  1803.     return false;
  1804. };
  1805.  
  1806. // sets the given doctype (useful when config.fullPage is true)
  1807. HTMLArea.prototype.setDoctype = function(doctype) {
  1808.     this.doctype = doctype;
  1809. };
  1810.  
  1811. /***************************************************
  1812.  *  Category: UTILITY FUNCTIONS
  1813.  ***************************************************/
  1814.  
  1815. // browser identification
  1816.  
  1817. HTMLArea.agt = navigator.userAgent.toLowerCase();
  1818. HTMLArea.is_ie       = ((HTMLArea.agt.indexOf("msie") != -1) && (HTMLArea.agt.indexOf("opera") == -1));
  1819. HTMLArea.is_opera  = (HTMLArea.agt.indexOf("opera") != -1);
  1820. HTMLArea.is_mac       = (HTMLArea.agt.indexOf("mac") != -1);
  1821. HTMLArea.is_mac_ie = (HTMLArea.is_ie && HTMLArea.is_mac);
  1822. HTMLArea.is_win_ie = (HTMLArea.is_ie && !HTMLArea.is_mac);
  1823. HTMLArea.is_gecko  = (navigator.product == "Gecko");
  1824.  
  1825. // variable used to pass the object to the popup editor window.
  1826. HTMLArea._object = null;
  1827.  
  1828. // function that returns a clone of the given object
  1829. HTMLArea.cloneObject = function(obj) {
  1830.     var newObj = new Object;
  1831.  
  1832.     // check for array objects
  1833.     if (obj.constructor.toString().indexOf("function Array(") == 1) {
  1834.         newObj = obj.constructor();
  1835.     }
  1836.  
  1837.     // check for function objects (as usual, IE is fucked up)
  1838.     if (obj.constructor.toString().indexOf("function Function(") == 1) {
  1839.         newObj = obj; // just copy reference to it
  1840.     } else for (var n in obj) {
  1841.         var node = obj[n];
  1842.         if (typeof node == 'object') { newObj[n] = HTMLArea.cloneObject(node); }
  1843.         else                         { newObj[n] = node; }
  1844.     }
  1845.  
  1846.     return newObj;
  1847. };
  1848.  
  1849. // FIXME!!! this should return false for IE < 5.5
  1850. HTMLArea.checkSupportedBrowser = function() {
  1851.     if (HTMLArea.is_gecko) {
  1852.         if (navigator.productSub < 20021201) {
  1853.             alert("You need at least Mozilla-1.3 Alpha.\n" +
  1854.                   "Sorry, your Gecko is not supported.");
  1855.             return false;
  1856.         }
  1857.         if (navigator.productSub < 20030210) {
  1858.             alert("Mozilla < 1.3 Beta is not supported!\n" +
  1859.                   "I'll try, though, but it might not work.");
  1860.         }
  1861.     }
  1862.     return HTMLArea.is_gecko || HTMLArea.is_ie;
  1863. };
  1864.  
  1865. // selection & ranges
  1866.  
  1867. // returns the current selection object
  1868. HTMLArea.prototype._getSelection = function() {
  1869.     if (HTMLArea.is_ie) {
  1870.         return this._doc.selection;
  1871.     } else {
  1872.         return this._iframe.contentWindow.getSelection();
  1873.     }
  1874. };
  1875.  
  1876. // returns a range for the current selection
  1877. HTMLArea.prototype._createRange = function(sel) {
  1878.     if (HTMLArea.is_ie) {
  1879.         return sel.createRange();
  1880.     } else {
  1881.         this.focusEditor();
  1882.         if (typeof sel != "undefined") {
  1883.             try {
  1884.                 return sel.getRangeAt(0);
  1885.             } catch(e) {
  1886.                 return this._doc.createRange();
  1887.             }
  1888.         } else {
  1889.             return this._doc.createRange();
  1890.         }
  1891.     }
  1892. };
  1893.  
  1894. // event handling
  1895.  
  1896. HTMLArea._addEvent = function(el, evname, func) {
  1897.     if (HTMLArea.is_ie) {
  1898.         el.attachEvent("on" + evname, func);
  1899.     } else {
  1900.         el.addEventListener(evname, func, true);
  1901.     }
  1902. };
  1903.  
  1904. HTMLArea._addEvents = function(el, evs, func) {
  1905.     for (var i in evs) {
  1906.         HTMLArea._addEvent(el, evs[i], func);
  1907.     }
  1908. };
  1909.  
  1910. HTMLArea._removeEvent = function(el, evname, func) {
  1911.     if (HTMLArea.is_ie) {
  1912.         el.detachEvent("on" + evname, func);
  1913.     } else {
  1914.         el.removeEventListener(evname, func, true);
  1915.     }
  1916. };
  1917.  
  1918. HTMLArea._removeEvents = function(el, evs, func) {
  1919.     for (var i in evs) {
  1920.         HTMLArea._removeEvent(el, evs[i], func);
  1921.     }
  1922. };
  1923.  
  1924. HTMLArea._stopEvent = function(ev) {
  1925.     if (HTMLArea.is_ie) {
  1926.         ev.cancelBubble = true;
  1927.         ev.returnValue = false;
  1928.     } else {
  1929.         ev.preventDefault();
  1930.         ev.stopPropagation();
  1931.     }
  1932. };
  1933.  
  1934. HTMLArea._removeClass = function(el, className) {
  1935.     if (!(el && el.className)) {
  1936.         return;
  1937.     }
  1938.     var cls = el.className.split(" ");
  1939.     var ar = new Array();
  1940.     for (var i = cls.length; i > 0;) {
  1941.         if (cls[--i] != className) {
  1942.             ar[ar.length] = cls[i];
  1943.         }
  1944.     }
  1945.     el.className = ar.join(" ");
  1946. };
  1947.  
  1948. HTMLArea._addClass = function(el, className) {
  1949.     // remove the class first, if already there
  1950.     HTMLArea._removeClass(el, className);
  1951.     el.className += " " + className;
  1952. };
  1953.  
  1954. HTMLArea._hasClass = function(el, className) {
  1955.     if (!(el && el.className)) {
  1956.         return false;
  1957.     }
  1958.     var cls = el.className.split(" ");
  1959.     for (var i = cls.length; i > 0;) {
  1960.         if (cls[--i] == className) {
  1961.             return true;
  1962.         }
  1963.     }
  1964.     return false;
  1965. };
  1966.  
  1967. HTMLArea.isBlockElement = function(el) {
  1968.     var blockTags = " body form textarea fieldset ul ol dl li div " +
  1969.         "p h1 h2 h3 h4 h5 h6 quote pre table thead " +
  1970.         "tbody tfoot tr td iframe address ";
  1971.     return (blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
  1972. };
  1973.  
  1974. HTMLArea.needsClosingTag = function(el) {
  1975.     var closingTags = " head script style div span tr td tbody table em strong font a title ";
  1976.     return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
  1977. };
  1978.  
  1979. // performs HTML encoding of some given string
  1980. HTMLArea.htmlEncode = function(str) {
  1981.     // we don't need regexp for that, but.. so be it for now.
  1982.     str = str.replace(/&/ig, "&");
  1983.     str = str.replace(/</ig, "<");
  1984.     str = str.replace(/>/ig, ">");
  1985.     str = str.replace(/\x22/ig, """);
  1986.     // \x22 means '"' -- we use hex reprezentation so that we don't disturb
  1987.     // JS compressors (well, at least mine fails.. ;)
  1988.     return str;
  1989. };
  1990.  
  1991. // Retrieves the HTML code from the given node.     This is a replacement for
  1992. // getting innerHTML, using standard DOM calls.
  1993. HTMLArea.getHTML = function(root, outputRoot, editor) {
  1994.     var html = "";
  1995.     switch (root.nodeType) {
  1996.         case 1: // Node.ELEMENT_NODE
  1997.         case 11: // Node.DOCUMENT_FRAGMENT_NODE
  1998.         var closed;
  1999.         var i;
  2000.         var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : '';
  2001.         if (HTMLArea.is_ie && root_tag == "head") {
  2002.             if (outputRoot)
  2003.                 html += "<head>";
  2004.             // lowercasize
  2005.             var save_multiline = RegExp.multiline;
  2006.             RegExp.multiline = true;
  2007.             var txt = root.innerHTML.replace(HTMLArea.RE_tagName, function(str, p1, p2) {
  2008.                 return p1 + p2.toLowerCase();
  2009.             });
  2010.             RegExp.multiline = save_multiline;
  2011.             html += txt;
  2012.             if (outputRoot)
  2013.                 html += "</head>";
  2014.             break;
  2015.         } else if (outputRoot) {
  2016.             closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root)));
  2017.             html = "<" + root.tagName.toLowerCase();
  2018.             var attrs = root.attributes;
  2019.             for (i = 0; i < attrs.length; ++i) {
  2020.                 var a = attrs.item(i);
  2021.                 if (!a.specified) {
  2022.                     continue;
  2023.                 }
  2024.                 var name = a.nodeName.toLowerCase();
  2025.                 if (/_moz|contenteditable|_msh/.test(name)) {
  2026.                     // avoid certain attributes
  2027.                     continue;
  2028.                 }
  2029.                 var value;
  2030.                 if (name != "style") {
  2031.                     // IE5.5 reports 25 when cellSpacing is
  2032.                     // 1; other values might be doomed too.
  2033.                     // For this reason we extract the
  2034.                     // values directly from the root node.
  2035.                     // I'm starting to HATE JavaScript
  2036.                     // development.  Browser differences
  2037.                     // suck.
  2038.                     //
  2039.                     // Using Gecko the values of href and src are converted to absolute links
  2040.                     // unless we get them using nodeValue()
  2041.                     if (typeof root[a.nodeName] != "undefined" && name != "href" && name != "src") {
  2042.                         value = root[a.nodeName];
  2043.                     } else {
  2044.                         value = a.nodeValue;
  2045.                         // IE seems not willing to return the original values - it converts to absolute
  2046.                         // links using a.nodeValue, a.value, a.stringValue, root.getAttribute("href")
  2047.                         // So we have to strip the baseurl manually -/
  2048.                         if (HTMLArea.is_ie && (name == "href" || name == "src")) {
  2049.                             value = editor.stripBaseURL(value);
  2050.                         }
  2051.                     }
  2052.                 } else { // IE fails to put style in attributes list
  2053.                     // FIXME: cssText reported by IE is UPPERCASE
  2054.                     value = root.style.cssText;
  2055.                 }
  2056.                 if (/(_moz|^$)/.test(value)) {
  2057.                     // Mozilla reports some special tags
  2058.                     // here; we don't need them.
  2059.                     continue;
  2060.                 }
  2061.                 html += " " + name + '="' + value + '"';
  2062.             }
  2063.             html += closed ? " />" : ">";
  2064.         }
  2065.         for (i = root.firstChild; i; i = i.nextSibling) {
  2066.             html += HTMLArea.getHTML(i, true, editor);
  2067.         }
  2068.         if (outputRoot && !closed) {
  2069.             html += "</" + root.tagName.toLowerCase() + ">";
  2070.         }
  2071.         break;
  2072.         case 3: // Node.TEXT_NODE
  2073.         // If a text node is alone in an element and all spaces, replace it with an non breaking one
  2074.         // This partially undoes the damage done by moz, which translates ' 's into spaces in the data element
  2075.         if ( !root.previousSibling && !root.nextSibling && root.data.match(/^\s*$/i) ) html = ' ';
  2076.         else html = HTMLArea.htmlEncode(root.data);
  2077.         break;
  2078.         case 8: // Node.COMMENT_NODE
  2079.         html = "<!--" + root.data + "-->";
  2080.         break;        // skip comments, for now.
  2081.     }
  2082.     return html;
  2083. };
  2084.  
  2085. HTMLArea.prototype.stripBaseURL = function(string) {
  2086.     var baseurl = this.config.baseURL;
  2087.  
  2088.     // strip to last directory in case baseurl points to a file
  2089.     baseurl = baseurl.replace(/[^\/]+$/, '');
  2090.     var basere = new RegExp(baseurl);
  2091.     string = string.replace(basere, "");
  2092.  
  2093.     // strip host-part of URL which is added by MSIE to links relative to server root
  2094.     baseurl = baseurl.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1');
  2095.     basere = new RegExp(baseurl);
  2096.     return string.replace(basere, "");
  2097. };
  2098.  
  2099. String.prototype.trim = function() {
  2100.     a = this.replace(/^\s+/, '');
  2101.     return a.replace(/\s+$/, '');
  2102. };
  2103.  
  2104. // creates a rgb-style color from a number
  2105. HTMLArea._makeColor = function(v) {
  2106.     if (typeof v != "number") {
  2107.         // already in rgb (hopefully); IE doesn't get here.
  2108.         return v;
  2109.     }
  2110.     // IE sends number; convert to rgb.
  2111.     var r = v & 0xFF;
  2112.     var g = (v >> 8) & 0xFF;
  2113.     var b = (v >> 16) & 0xFF;
  2114.     return "rgb(" + r + "," + g + "," + b + ")";
  2115. };
  2116.  
  2117. // returns hexadecimal color representation from a number or a rgb-style color.
  2118. HTMLArea._colorToRgb = function(v) {
  2119.     if (!v)
  2120.         return '';
  2121.  
  2122.     // returns the hex representation of one byte (2 digits)
  2123.     function hex(d) {
  2124.         return (d < 16) ? ("0" + d.toString(16)) : d.toString(16);
  2125.     };
  2126.  
  2127.     if (typeof v == "number") {
  2128.         // we're talking to IE here
  2129.         var r = v & 0xFF;
  2130.         var g = (v >> 8) & 0xFF;
  2131.         var b = (v >> 16) & 0xFF;
  2132.         return "#" + hex(r) + hex(g) + hex(b);
  2133.     }
  2134.  
  2135.     if (v.substr(0, 3) == "rgb") {
  2136.         // in rgb(...) form -- Mozilla
  2137.         var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/;
  2138.         if (v.match(re)) {
  2139.             var r = parseInt(RegExp.$1);
  2140.             var g = parseInt(RegExp.$2);
  2141.             var b = parseInt(RegExp.$3);
  2142.             return "#" + hex(r) + hex(g) + hex(b);
  2143.         }
  2144.         // doesn't match RE?!  maybe uses percentages or float numbers
  2145.         // -- FIXME: not yet implemented.
  2146.         return null;
  2147.     }
  2148.  
  2149.     if (v.substr(0, 1) == "#") {
  2150.         // already hex rgb (hopefully :D )
  2151.         return v;
  2152.     }
  2153.  
  2154.     // if everything else fails ;)
  2155.     return null;
  2156. };
  2157.  
  2158. // modal dialogs for Mozilla (for IE we're using the showModalDialog() call).
  2159.  
  2160. // receives an URL to the popup dialog and a function that receives one value;
  2161. // this function will get called after the dialog is closed, with the return
  2162. // value of the dialog.
  2163. HTMLArea.prototype._popupDialog = function(url, action, init) {
  2164.     Dialog(this.popupURL(url), action, init);
  2165. };
  2166.  
  2167. // paths
  2168.  
  2169. HTMLArea.prototype.imgURL = function(file, plugin) {
  2170.     if (typeof plugin == "undefined")
  2171.         return _editor_url + file;
  2172.     else
  2173.         return _editor_url + "plugins/" + plugin + "/img/" + file;
  2174. };
  2175.  
  2176. HTMLArea.prototype.popupURL = function(file) {
  2177.     var url = "";
  2178.     if (file.match(/^plugin:\/\/(.*?)\/(.*)/)) {
  2179.         var plugin = RegExp.$1;
  2180.         var popup = RegExp.$2;
  2181.         if (!/\.html$/.test(popup))
  2182.             popup += ".html";
  2183.         url = _editor_url + "plugins/" + plugin + "/popups/" + popup;
  2184.     } else
  2185.         url = _editor_url + this.config.popupURL + file;
  2186.     return url;
  2187. };
  2188.  
  2189. /**
  2190.  * FIX: Internet Explorer returns an item having the _name_ equal to the given
  2191.  * id, even if it's not having any id.  This way it can return a different form
  2192.  * field even if it's not a textarea.  This workarounds the problem by
  2193.  * specifically looking to search only elements having a certain tag name.
  2194.  */
  2195. HTMLArea.getElementById = function(tag, id) {
  2196.     var el, i, objs = document.getElementsByTagName(tag);
  2197.     for (i = objs.length; --i >= 0 && (el = objs[i]);)
  2198.         if (el.id == id)
  2199.             return el;
  2200.     return null;
  2201. };
  2202.  
  2203.  
  2204.  
  2205. // EOF
  2206. // Local variables: //
  2207. // c-basic-offset:8 //
  2208. // indent-tabs-mode:t //
  2209. // End: //
  2210.