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