source: trunk/phpgwapi/js/htmlarea/htmlarea.js @ 2

Revision 2, 69.2 KB checked in by niltonneto, 17 years ago (diff)

Removida todas as tags usadas pelo CVS ($Id, $Source).
Primeira versão no CVS externo.

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