Annotation of loncom/html/htmlarea/htmlarea.js, revision 1.2
1.1 www 1: //
2: // htmlArea v3.0 - Copyright (c) 2002 interactivetools.com, inc.
3: // This copyright notice MUST stay intact for use (see license.txt).
4: //
5: // A free WYSIWYG editor replacement for <textarea> fields.
6: // For full source code and docs, visit http://www.interactivetools.com/
7: //
8: // Version 3.0 developed by Mihai Bazon for InteractiveTools.
9: // http://students.infoiasi.ro/~mishoo
10: //
1.2 ! www 11: // $Id: htmlarea.js,v 1.1 2004/02/18 08:07:15 www Exp $
1.1 www 12:
13: // Creates a new HTMLArea object. Tries to replace the textarea with the given
14: // ID with it.
15: function HTMLArea(textarea, config) {
16: if (HTMLArea.checkSupportedBrowser()) {
17: if (typeof config == "undefined") {
18: this.config = new HTMLArea.Config();
19: } else {
20: this.config = config;
21: }
22: this._htmlArea = null;
23: this._textArea = textarea;
24: this._editMode = "wysiwyg";
25: this.plugins = {};
26: this._timerToolbar = null;
27: this._mdoc = document; // cache the document, we need it in plugins
28: }
29: };
30:
31: HTMLArea.Config = function () {
32: this.version = "3.0";
33:
34: this.width = "auto";
35: this.height = "auto";
36:
37: // enable creation of a status bar?
38: this.statusBar = true;
39:
40: // the next parameter specifies whether the toolbar should be included
41: // in the size or not.
42: this.sizeIncludesToolbar = true;
43:
44: // style included in the iframe document
45: this.pageStyle = "body { background-color: #fff; font-family: verdana,sans-serif; }";
46: if (typeof _editor_url != "undefined") {
47: this.editorURL = _editor_url;
48: } else {
49: this.editorURL = "";
50: }
51:
52: // URL-s
53: this.imgURL = "images/";
54: this.popupURL = "popups/";
55:
56: // configuration for plugins
57: this.plugins = {};
58:
59: /** CUSTOMIZING THE TOOLBAR
60: * -------------------------
61: *
62: * It is recommended that you customize the toolbar contents in an
63: * external file (i.e. the one calling HTMLArea) and leave this one
64: * unchanged. That's because when we (InteractiveTools.com) release a
65: * new official version, it's less likely that you will have problems
66: * upgrading HTMLArea.
67: */
68: this.toolbar = [
69: [ "fontname", "space",
70: "fontsize", "space",
71: "formatblock", "space",
72: "bold", "italic", "underline", "separator",
73: "strikethrough", "subscript", "superscript", "separator",
74: "copy", "cut", "paste", "space", "undo", "redo" ],
75:
76: [ "justifyleft", "justifycenter", "justifyright", "justifyfull", "separator",
77: "insertorderedlist", "insertunorderedlist", "outdent", "indent", "separator",
78: "forecolor", "hilitecolor", "textindicator", "separator",
79: "inserthorizontalrule", "createlink", "insertimage", "inserttable", "htmlmode", "separator",
80: "popupeditor", "separator", "showhelp", "about" ]
81: ];
82:
83: this.fontname = {
84: "Arial": 'arial,helvetica,sans-serif',
85: "Courier New": 'courier new,courier,monospace',
86: "Georgia": 'georgia,times new roman,times,serif',
87: "Tahoma": 'tahoma,arial,helvetica,sans-serif',
88: "Times New Roman": 'times new roman,times,serif',
89: "Verdana": 'verdana,arial,helvetica,sans-serif',
90: "impact": 'impact',
91: "WingDings": 'wingdings'
92: };
93:
94: this.fontsize = {
95: "1 (8 pt)": "1",
96: "2 (10 pt)": "2",
97: "3 (12 pt)": "3",
98: "4 (14 pt)": "4",
99: "5 (18 pt)": "5",
100: "6 (24 pt)": "6",
101: "7 (36 pt)": "7"
102: };
103:
104: this.formatblock = {
105: "Heading 1": "h1",
106: "Heading 2": "h2",
107: "Heading 3": "h3",
108: "Heading 4": "h4",
109: "Heading 5": "h5",
110: "Heading 6": "h6",
111: "Normal": "p",
112: "Address": "address",
113: "Formatted": "pre"
114: };
115:
116: this.customSelects = {};
117:
118: function cut_copy_paste(e, cmd, obj) {
119: try {
120: e.execCommand(cmd);
121: } catch (e) {
122: if (HTMLArea.is_gecko) {
123: alert("Some revisions of Mozilla/Gecko do not support programatic " +
124: "access to cut/copy/paste functions, for security reasons. " +
125: "Your browser is one of them. Please use the standard key combinations:\n" +
126: "CTRL-X for cut, CTRL-C for copy, CTRL-V for paste.");
127: obj.element.style.display = "none";
128: }
129: }
130: };
131:
132: // ADDING CUSTOM BUTTONS: please read below!
133: // format of the btnList elements is "ID: [ ToolTip, Icon, Enabled in text mode?, ACTION ]"
134: // - ID: unique ID for the button. If the button calls document.execCommand
135: // it's wise to give it the same name as the called command.
136: // - ACTION: function that gets called when the button is clicked.
137: // it has the following prototype:
138: // function(editor, buttonName)
139: // - editor is the HTMLArea object that triggered the call
140: // - buttonName is the ID of the clicked button
141: // These 2 parameters makes it possible for you to use the same
142: // handler for more HTMLArea objects or for more different buttons.
143: // - ToolTip: default tooltip, for cases when it is not defined in the -lang- file (HTMLArea.I18N)
144: // - Icon: path to an icon image file for the button (TODO: use one image for all buttons!)
145: // - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time.
146: this.btnList = {
147: bold: [ "Bold", "images/ed_format_bold.gif", false, function(e) {e.execCommand("bold");} ],
148: italic: [ "Italic", "images/ed_format_italic.gif", false, function(e) {e.execCommand("italic");} ],
149: underline: [ "Underline", "images/ed_format_underline.gif", false, function(e) {e.execCommand("underline");} ],
150: strikethrough: [ "Strikethrough", "images/ed_format_strike.gif", false, function(e) {e.execCommand("strikethrough");} ],
151: subscript: [ "Subscript", "images/ed_format_sub.gif", false, function(e) {e.execCommand("subscript");} ],
152: superscript: [ "Superscript", "images/ed_format_sup.gif", false, function(e) {e.execCommand("superscript");} ],
153: justifyleft: [ "Justify Left", "images/ed_align_left.gif", false, function(e) {e.execCommand("justifyleft");} ],
154: justifycenter: [ "Justify Center", "images/ed_align_center.gif", false, function(e) {e.execCommand("justifycenter");} ],
155: justifyright: [ "Justify Right", "images/ed_align_right.gif", false, function(e) {e.execCommand("justifyright");} ],
156: justifyfull: [ "Justify Full", "images/ed_align_justify.gif", false, function(e) {e.execCommand("justifyfull");} ],
157: insertorderedlist: [ "Ordered List", "images/ed_list_num.gif", false, function(e) {e.execCommand("insertorderedlist");} ],
158: insertunorderedlist: [ "Bulleted List", "images/ed_list_bullet.gif", false, function(e) {e.execCommand("insertunorderedlist");} ],
159: outdent: [ "Decrease Indent", "images/ed_indent_less.gif", false, function(e) {e.execCommand("outdent");} ],
160: indent: [ "Increase Indent", "images/ed_indent_more.gif", false, function(e) {e.execCommand("indent");} ],
161: forecolor: [ "Font Color", "images/ed_color_fg.gif", false, function(e) {e.execCommand("forecolor");} ],
162: hilitecolor: [ "Background Color", "images/ed_color_bg.gif", false, function(e) {e.execCommand("hilitecolor");} ],
163: inserthorizontalrule: [ "Horizontal Rule", "images/ed_hr.gif", false, function(e) {e.execCommand("inserthorizontalrule");} ],
164: createlink: [ "Insert Web Link", "images/ed_link.gif", false, function(e) {e.execCommand("createlink", true);} ],
165: insertimage: [ "Insert Image", "images/ed_image.gif", false, function(e) {e.execCommand("insertimage");} ],
166: inserttable: [ "Insert Table", "images/insert_table.gif", false, function(e) {e.execCommand("inserttable");} ],
167: htmlmode: [ "Toggle HTML Source", "images/ed_html.gif", true, function(e) {e.execCommand("htmlmode");} ],
168: popupeditor: [ "Enlarge Editor", "images/fullscreen_maximize.gif", true, function(e) {e.execCommand("popupeditor");} ],
169: about: [ "About this editor", "images/ed_about.gif", true, function(e) {e.execCommand("about");} ],
170: showhelp: [ "Help using editor", "images/ed_help.gif", true, function(e) {e.execCommand("showhelp");} ],
171: undo: [ "Undoes your last action", "images/ed_undo.gif", false, function(e) {e.execCommand("undo");} ],
172: redo: [ "Redoes your last action", "images/ed_redo.gif", false, function(e) {e.execCommand("redo");} ],
173: cut: [ "Cut selection", "images/ed_cut.gif", false, cut_copy_paste ],
174: copy: [ "Copy selection", "images/ed_copy.gif", false, cut_copy_paste ],
175: paste: [ "Paste from clipboard", "images/ed_paste.gif", false, cut_copy_paste ]
176: };
177: /* ADDING CUSTOM BUTTONS
178: * ---------------------
179: *
180: * It is recommended that you add the custom buttons in an external
181: * file and leave this one unchanged. That's because when we
182: * (InteractiveTools.com) release a new official version, it's less
183: * likely that you will have problems upgrading HTMLArea.
184: *
185: * Example on how to add a custom button when you construct the HTMLArea:
186: *
187: * var editor = new HTMLArea("your_text_area_id");
188: * var cfg = editor.config; // this is the default configuration
189: * cfg.btnList["my-hilite"] =
190: * [ function(editor) { editor.surroundHTML('<span style="background:yellow">', '</span>'); }, // action
191: * "Highlight selection", // tooltip
192: * "my_hilite.gif", // image
193: * false // disabled in text mode
194: * ];
195: * cfg.toolbar.push(["linebreak", "my-hilite"]); // add the new button to the toolbar
196: *
197: * An alternate (also more convenient and recommended) way to
198: * accomplish this is to use the registerButton function below.
199: */
200: // initialize tooltips from the I18N module
201: for (var i in this.btnList) {
202: var btn = this.btnList[i];
203: if (typeof HTMLArea.I18N.tooltips[i] != "undefined") {
204: btn[0] = HTMLArea.I18N.tooltips[i];
205: }
206: }
207: };
208:
209: /** Helper function: register a new button with the configuration. It can be
210: * called with all 5 arguments, or with only one (first one). When called with
211: * only one argument it must be an object with the following properties: id,
212: * tooltip, image, textMode, action. Examples:
213: *
214: * 1. config.registerButton("my-hilite", "Hilite text", "my-hilite.gif", false, function(editor) {...});
215: * 2. config.registerButton({
216: * id : "my-hilite", // the ID of your button
217: * tooltip : "Hilite text", // the tooltip
218: * image : "my-hilite.gif", // image to be displayed in the toolbar
219: * textMode : false, // disabled in text mode
220: * action : function(editor) { // called when the button is clicked
221: * editor.surroundHTML('<span class="hilite">', '</span>');
222: * },
223: * context : "p" // will be disabled if outside a <p> element
224: * });
225: */
226: HTMLArea.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context) {
227: var the_id;
228: if (typeof id == "string") {
229: the_id = id;
230: } else if (typeof id == "object") {
231: the_id = id.id;
232: } else {
233: alert("ERROR [HTMLArea.Config::registerButton]:\ninvalid arguments");
234: return false;
235: }
236: // check for existing id
237: if (typeof this.customSelects[the_id] != "undefined") {
238: alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
239: }
240: if (typeof this.btnList[the_id] != "undefined") {
241: alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
242: }
243: switch (typeof id) {
244: case "string": this.btnList[id] = [ tooltip, image, textMode, action, context ]; break;
245: case "object": this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; break;
246: }
247: };
248:
249: /** The following helper function registers a dropdown box with the editor
250: * configuration. You still have to add it to the toolbar, same as with the
251: * buttons. Call it like this:
252: *
253: * FIXME: add example
254: */
255: HTMLArea.Config.prototype.registerDropdown = function(object) {
256: // check for existing id
257: if (typeof this.customSelects[object.id] != "undefined") {
258: alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
259: }
260: if (typeof this.btnList[object.id] != "undefined") {
261: alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
262: }
263: this.customSelects[object.id] = object;
264: };
265:
266: /** Helper function: replace all TEXTAREA-s in the document with HTMLArea-s. */
267: HTMLArea.replaceAll = function(config) {
268: var tas = document.getElementsByTagName("textarea");
269: for (var i = tas.length; i > 0; (new HTMLArea(tas[--i], config)).generate());
270: };
271:
272: /** Helper function: replaces the TEXTAREA with the given ID with HTMLArea. */
273: HTMLArea.replace = function(id, config) {
274: var ta = document.getElementById(id);
275: return ta ? (new HTMLArea(ta, config)).generate() : null;
276: };
277:
278: // Creates the toolbar and appends it to the _htmlarea
279: HTMLArea.prototype._createToolbar = function () {
280: var editor = this; // to access this in nested functions
281:
282: var toolbar = document.createElement("div");
283: this._toolbar = toolbar;
284: toolbar.className = "toolbar";
285: toolbar.unselectable = "1";
286: var tb_row = null;
287: var tb_objects = new Object();
288: this._toolbarObjects = tb_objects;
289:
290: // creates a new line in the toolbar
291: function newLine() {
292: var table = document.createElement("table");
293: table.border = "0px";
294: table.cellSpacing = "0px";
295: table.cellPadding = "0px";
296: toolbar.appendChild(table);
297: // TBODY is required for IE, otherwise you don't see anything
298: // in the TABLE.
299: var tb_body = document.createElement("tbody");
300: table.appendChild(tb_body);
301: tb_row = document.createElement("tr");
302: tb_body.appendChild(tb_row);
303: }; // END of function: newLine
304: // init first line
305: newLine();
306:
307: // updates the state of a toolbar element. This function is member of
308: // a toolbar element object (unnamed objects created by createButton or
309: // createSelect functions below).
310: function setButtonStatus(id, newval) {
311: var oldval = this[id];
312: var el = this.element;
313: if (oldval != newval) {
314: switch (id) {
315: case "enabled":
316: if (newval) {
317: HTMLArea._removeClass(el, "buttonDisabled");
318: el.disabled = false;
319: } else {
320: HTMLArea._addClass(el, "buttonDisabled");
321: el.disabled = true;
322: }
323: break;
324: case "active":
325: if (newval) {
326: HTMLArea._addClass(el, "buttonPressed");
327: } else {
328: HTMLArea._removeClass(el, "buttonPressed");
329: }
330: break;
331: }
332: this[id] = newval;
333: }
334: }; // END of function: setButtonStatus
335:
336: // this function will handle creation of combo boxes. Receives as
337: // parameter the name of a button as defined in the toolBar config.
338: // This function is called from createButton, above, if the given "txt"
339: // doesn't match a button.
340: function createSelect(txt) {
341: var options = null;
342: var el = null;
343: var cmd = null;
344: var customSelects = editor.config.customSelects;
345: var context = null;
346: switch (txt) {
347: case "fontsize":
348: case "fontname":
349: case "formatblock":
350: // the following line retrieves the correct
351: // configuration option because the variable name
352: // inside the Config object is named the same as the
353: // button/select in the toolbar. For instance, if txt
354: // == "formatblock" we retrieve config.formatblock (or
355: // a different way to write it in JS is
356: // config["formatblock"].
357: options = editor.config[txt];
358: cmd = txt;
359: break;
360: default:
361: // try to fetch it from the list of registered selects
362: cmd = txt;
363: var dropdown = customSelects[cmd];
364: if (typeof dropdown != "undefined") {
365: options = dropdown.options;
366: context = dropdown.context;
367: } else {
368: alert("ERROR [createSelect]:\nCan't find the requested dropdown definition");
369: }
370: break;
371: }
372: if (options) {
373: el = document.createElement("select");
374: var obj = {
375: name : txt, // field name
376: element : el, // the UI element (SELECT)
377: enabled : true, // is it enabled?
378: text : false, // enabled in text mode?
379: cmd : cmd, // command ID
380: state : setButtonStatus, // for changing state
381: context : context
382: };
383: tb_objects[txt] = obj;
384: for (var i in options) {
385: var op = document.createElement("option");
386: op.appendChild(document.createTextNode(i));
387: op.value = options[i];
388: el.appendChild(op);
389: }
390: HTMLArea._addEvent(el, "change", function () {
391: editor._comboSelected(el, txt);
392: });
393: }
394: return el;
395: }; // END of function: createSelect
396:
397: // appends a new button to toolbar
398: function createButton(txt) {
399: // the element that will be created
400: var el = null;
401: var btn = null;
402: switch (txt) {
403: case "separator":
404: el = document.createElement("div");
405: el.className = "separator";
406: break;
407: case "space":
408: el = document.createElement("div");
409: el.className = "space";
410: break;
411: case "linebreak":
412: newLine();
413: return false;
414: case "textindicator":
415: el = document.createElement("div");
416: el.appendChild(document.createTextNode("A"));
417: el.className = "indicator";
418: el.title = HTMLArea.I18N.tooltips.textindicator;
419: var obj = {
420: name : txt, // the button name (i.e. 'bold')
421: element : el, // the UI element (DIV)
422: enabled : true, // is it enabled?
423: active : false, // is it pressed?
424: text : false, // enabled in text mode?
425: cmd : "textindicator", // the command ID
426: state : setButtonStatus // for changing state
427: };
428: tb_objects[txt] = obj;
429: break;
430: default:
431: btn = editor.config.btnList[txt];
432: }
433: if (!el && btn) {
434: el = document.createElement("div");
435: el.title = btn[0];
436: el.className = "button";
437: // let's just pretend we have a button object, and
438: // assign all the needed information to it.
439: var obj = {
440: name : txt, // the button name (i.e. 'bold')
441: element : el, // the UI element (DIV)
442: enabled : true, // is it enabled?
443: active : false, // is it pressed?
444: text : btn[2], // enabled in text mode?
445: cmd : btn[3], // the command ID
446: state : setButtonStatus, // for changing state
447: context : btn[4] || null // enabled in a certain context?
448: };
449: tb_objects[txt] = obj;
450: // handlers to emulate nice flat toolbar buttons
451: HTMLArea._addEvent(el, "mouseover", function () {
452: if (obj.enabled) {
453: HTMLArea._addClass(el, "buttonHover");
454: }
455: });
456: HTMLArea._addEvent(el, "mouseout", function () {
457: if (obj.enabled) with (HTMLArea) {
458: _removeClass(el, "buttonHover");
459: _removeClass(el, "buttonActive");
460: (obj.active) && _addClass(el, "buttonPressed");
461: }
462: });
463: HTMLArea._addEvent(el, "mousedown", function (ev) {
464: if (obj.enabled) with (HTMLArea) {
465: _addClass(el, "buttonActive");
466: _removeClass(el, "buttonPressed");
467: _stopEvent(is_ie ? window.event : ev);
468: }
469: });
470: // when clicked, do the following:
471: HTMLArea._addEvent(el, "click", function (ev) {
472: if (obj.enabled) with (HTMLArea) {
473: _removeClass(el, "buttonActive");
474: _removeClass(el, "buttonHover");
475: obj.cmd(editor, obj.name, obj);
476: _stopEvent(is_ie ? window.event : ev);
477: }
478: });
479: var img = document.createElement("img");
480: img.src = editor.imgURL(btn[1]);
481: img.style.width = "18px";
482: img.style.height = "18px";
483: el.appendChild(img);
484: } else if (!el) {
485: el = createSelect(txt);
486: }
487: if (el) {
488: var tb_cell = document.createElement("td");
489: tb_row.appendChild(tb_cell);
490: tb_cell.appendChild(el);
491: } else {
492: alert("FIXME: Unknown toolbar item: " + txt);
493: }
494: return el;
495: };
496:
497: var first = true;
498: for (var i in this.config.toolbar) {
499: if (!first) {
500: createButton("linebreak");
501: } else {
502: first = false;
503: }
504: var group = this.config.toolbar[i];
505: for (var j in group) {
506: var code = group[j];
507: if (/^([IT])\[(.*?)\]/.test(code)) {
508: // special case, create text label
509: var l7ed = RegExp.$1 == "I"; // localized?
510: var label = RegExp.$2;
511: if (l7ed) {
512: label = HTMLArea.I18N.custom[label];
513: }
514: var tb_cell = document.createElement("td");
515: tb_row.appendChild(tb_cell);
516: tb_cell.className = "label";
517: tb_cell.innerHTML = label;
518: } else {
519: createButton(code);
520: }
521: }
522: }
523:
524: this._htmlArea.appendChild(toolbar);
525: };
526:
527: HTMLArea.prototype._createStatusBar = function() {
528: var div = document.createElement("div");
529: div.className = "statusBar";
530: this._htmlArea.appendChild(div);
531: this._statusBar = div;
532: div.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
533: // creates a holder for the path view
534: div = document.createElement("span");
535: div.className = "statusBarTree";
536: this._statusBarTree = div;
537: this._statusBar.appendChild(div);
538: if (!this.config.statusBar) {
539: // disable it...
540: div.style.display = "none";
541: }
542: };
543:
544: // Creates the HTMLArea object and replaces the textarea with it.
545: HTMLArea.prototype.generate = function () {
546: var editor = this; // we'll need "this" in some nested functions
547: // get the textarea
548: var textarea = this._textArea;
549: if (typeof textarea == "string") {
550: // it's not element but ID
551: this._textArea = textarea = document.getElementById(textarea);
552: }
553: this._ta_size = {
554: w: textarea.offsetWidth,
555: h: textarea.offsetHeight
556: };
557: textarea.style.display = "none";
558:
559: // create the editor framework
560: var htmlarea = document.createElement("div");
561: htmlarea.className = "htmlarea";
562: this._htmlArea = htmlarea;
563:
564: // insert the editor before the textarea.
565: textarea.parentNode.insertBefore(htmlarea, textarea);
566:
567: if (textarea.form) {
568: // we have a form, on submit get the HTMLArea content and
569: // update original textarea.
570: textarea.form.onsubmit = function() {
571: editor._textArea.value = editor.getHTML();
572: };
573: }
574:
575: // add a handler for the "back/forward" case -- on body.unload we save
576: // the HTML content into the original textarea.
577: window.onunload = function() {
578: editor._textArea.value = editor.getHTML();
579: };
580:
581: // creates & appends the toolbar
582: this._createToolbar();
583:
584: // create the IFRAME
585: var iframe = document.createElement("iframe");
586: htmlarea.appendChild(iframe);
587:
588: this._iframe = iframe;
589:
590: // creates & appends the status bar, if the case
591: this._createStatusBar();
592:
593: // remove the default border as it keeps us from computing correctly
594: // the sizes. (somebody tell me why doesn't this work in IE)
595:
596: if (!HTMLArea.is_ie) {
597: iframe.style.borderWidth = "1px";
598: // iframe.frameBorder = "1";
599: // iframe.marginHeight = "0";
600: // iframe.marginWidth = "0";
601: }
602:
603: // size the IFRAME according to user's prefs or initial textarea
604: var height = (this.config.height == "auto" ? (this._ta_size.h + "px") : this.config.height);
605: height = parseInt(height);
606: var width = (this.config.width == "auto" ? (this._ta_size.w + "px") : this.config.width);
607: width = parseInt(width);
608:
609: if (!HTMLArea.is_ie) {
610: height -= 2;
611: width -= 2;
612: }
613:
614: iframe.style.width = width + "px";
615: if (this.config.sizeIncludesToolbar) {
616: // substract toolbar height
617: height -= this._toolbar.offsetHeight;
618: height -= this._statusBar.offsetHeight;
619: }
620: if (height < 0) {
621: height = 0;
622: }
623: iframe.style.height = height + "px";
624:
625: // the editor including the toolbar now have the same size as the
626: // original textarea.. which means that we need to reduce that a bit.
627: textarea.style.width = iframe.style.width;
628: textarea.style.height = iframe.style.height;
629:
630: // IMPORTANT: we have to allow Mozilla a short time to recognize the
631: // new frame. Otherwise we get a stupid exception.
632: function initIframe() {
633: var doc = editor._iframe.contentWindow.document;
634: if (!doc) {
635: // Try again..
636: // FIXME: don't know what else to do here. Normally
637: // we'll never reach this point.
638: if (HTMLArea.is_gecko) {
639: setTimeout(initIframe, 10);
640: return false;
641: } else {
642: alert("ERROR: IFRAME can't be initialized.");
643: }
644: }
645: if (HTMLArea.is_gecko) {
646: // enable editable mode for Mozilla
647: doc.designMode = "on";
648: }
649: editor._doc = doc;
650: doc.open();
651: var html = "<html>\n";
652: html += "<head>\n";
653: html += "<style>" + editor.config.pageStyle + "</style>\n";
654: html += "</head>\n";
655: html += "<body>\n";
656: html += editor._textArea.value;
657: html += "</body>\n";
658: html += "</html>";
659: doc.write(html);
660: doc.close();
661:
662: if (HTMLArea.is_ie) {
663: // enable editable mode for IE. For some reason this
664: // doesn't work if done in the same place as for Gecko
665: // (above).
666: doc.body.contentEditable = true;
667: }
668:
669: editor.focusEditor();
670: // intercept some events; for updating the toolbar & keyboard handlers
671: HTMLArea._addEvents
672: (doc, ["keydown", "keypress", "mousedown", "mouseup", "drag"],
673: function (event) {
674: return editor._editorEvent(HTMLArea.is_ie ? editor._iframe.contentWindow.event : event);
675: });
676: editor.updateToolbar();
677: };
678: setTimeout(initIframe, HTMLArea.is_gecko ? 10 : 0);
679: };
680:
681: // Switches editor mode; parameter can be "textmode" or "wysiwyg". If no
682: // parameter was passed this function toggles between modes.
683: HTMLArea.prototype.setMode = function(mode) {
684: if (typeof mode == "undefined") {
685: mode = ((this._editMode == "textmode") ? "wysiwyg" : "textmode");
686: }
687: switch (mode) {
688: case "textmode":
689: this._textArea.value = this.getHTML();
690: this._iframe.style.display = "none";
691: this._textArea.style.display = "block";
692: if (this.config.statusBar) {
693: this._statusBar.innerHTML = HTMLArea.I18N.msg["TEXT_MODE"];
694: }
695: break;
696: case "wysiwyg":
697: if (HTMLArea.is_gecko) {
698: // disable design mode before changing innerHTML
699: this._doc.designMode = "off";
700: }
701: this._doc.body.innerHTML = this.getHTML();
702: this._iframe.style.display = "block";
703: this._textArea.style.display = "none";
704: if (HTMLArea.is_gecko) {
705: // we need to refresh that info for Moz-1.3a
706: this._doc.designMode = "on";
707: }
708: if (this.config.statusBar) {
709: this._statusBar.innerHTML = '';
710: this._statusBar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
711: this._statusBar.appendChild(this._statusBarTree);
712: }
713: break;
714: default:
715: alert("Mode <" + mode + "> not defined!");
716: return false;
717: }
718: this._editMode = mode;
719: this.focusEditor();
720: };
721:
722: /***************************************************
723: * Category: PLUGINS
724: ***************************************************/
725:
726: // Create the specified plugin and register it with this HTMLArea
727: HTMLArea.prototype.registerPlugin = function(pluginName) {
728: this.plugins[pluginName] = eval("new " + pluginName + "(this);");
729: };
730:
731: // static function that loads the required plugin and lang file, based on the
732: // language loaded already for HTMLArea. You better make sure that the plugin
733: // _has_ that language, otherwise shit might happen ;-)
734: HTMLArea.loadPlugin = function(pluginName) {
735: var editorurl = '';
736: if (typeof _editor_url != "undefined") {
737: editorurl = _editor_url + "/";
738: }
739: var dir = editorurl + "plugins/" + pluginName;
740: var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g,
741: function (str, l1, l2, l3) {
742: return l1 + "-" + l2.toLowerCase() + l3;
743: }).toLowerCase() + ".js";
744: document.write("<script type='text/javascript' src='" + dir + "/" + plugin + "'></script>");
745: document.write("<script type='text/javascript' src='" + dir + "/lang/" + HTMLArea.I18N.lang + ".js'></script>");
746: };
747:
748: /***************************************************
749: * Category: EDITOR UTILITIES
750: ***************************************************/
751:
752: HTMLArea.prototype.forceRedraw = function() {
753: this._doc.body.style.visibility = "hidden";
754: this._doc.body.style.visibility = "visible";
755: // this._doc.body.innerHTML = this.getInnerHTML();
756: };
757:
758: // focuses the iframe window. returns a reference to the editor document.
759: HTMLArea.prototype.focusEditor = function() {
760: switch (this._editMode) {
761: case "wysiwyg" : this._iframe.contentWindow.focus(); break;
762: case "textmode": this._textArea.focus(); break;
763: default : alert("ERROR: mode " + this._editMode + " is not defined");
764: }
765: return this._doc;
766: };
767:
768: // updates enabled/disable/active state of the toolbar elements
769: HTMLArea.prototype.updateToolbar = function(noStatus) {
770: var doc = this._doc;
771: var text = (this._editMode == "textmode");
772: var ancestors = null;
773: if (!text) {
774: ancestors = this.getAllAncestors();
775: if (this.config.statusBar && !noStatus) {
776: this._statusBarTree.innerHTML = ''; // clear
777: for (var i = ancestors.length; --i >= 0;) {
778: var el = ancestors[i];
779: if (!el) {
780: // hell knows why we get here; this
781: // could be a classic example of why
782: // it's good to check for conditions
783: // that are impossible to happen ;-)
784: continue;
785: }
786: var a = document.createElement("a");
787: a.href = "#";
788: a.el = el;
789: a.editor = this;
790: a.onclick = function() {
791: this.blur();
792: this.editor.selectNodeContents(this.el);
793: this.editor.updateToolbar(true);
794: return false;
795: };
796: a.oncontextmenu = function() {
797: // TODO: add context menu here
798: this.blur();
799: var info = "Inline style:\n\n";
800: info += this.el.style.cssText.split(/;\s*/).join(";\n");
801: alert(info);
802: return false;
803: };
804: var txt = el.tagName.toLowerCase();
805: a.title = el.style.cssText;
806: if (el.id) {
807: txt += "#" + el.id;
808: }
809: if (el.className) {
810: txt += "." + el.className;
811: }
812: a.appendChild(document.createTextNode(txt));
813: this._statusBarTree.appendChild(a);
814: if (i != 0) {
815: this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb)));
816: }
817: }
818: }
819: }
820: for (var i in this._toolbarObjects) {
821: var btn = this._toolbarObjects[i];
822: var cmd = i;
823: var inContext = true;
824: if (btn.context && !text) {
825: inContext = false;
826: var context = btn.context;
827: var attrs = [];
828: if (/(.*)\[(.*?)\]/.test(context)) {
829: context = RegExp.$1;
830: attrs = RegExp.$2.split(",");
831: }
832: context = context.toLowerCase();
833: var match = (context == "*");
834: for (var k in ancestors) {
835: if (!ancestors[k]) {
836: // the impossible really happens.
837: continue;
838: }
839: if (match || (ancestors[k].tagName.toLowerCase() == context)) {
840: inContext = true;
841: for (var ka in attrs) {
842: if (!eval("ancestors[k]." + attrs[ka])) {
843: inContext = false;
844: break;
845: }
846: }
847: if (inContext) {
848: break;
849: }
850: }
851: }
852: }
853: btn.state("enabled", (!text || btn.text) && inContext);
854: if (typeof cmd == "function") {
855: continue;
856: }
857: // look-it-up in the custom dropdown boxes
858: var dropdown = this.config.customSelects[cmd];
859: if ((!text || btn.text) && (typeof dropdown != "undefined")) {
860: dropdown.refresh(this);
861: continue;
862: }
863: switch (cmd) {
864: case "fontname":
865: case "fontsize":
866: case "formatblock":
867: if (!text) {
868: var value = ("" + doc.queryCommandValue(cmd)).toLowerCase();
869: if (!value) {
870: // FIXME: what do we do here?
871: break;
872: }
873: // HACK -- retrieve the config option for this
874: // combo box. We rely on the fact that the
875: // variable in config has the same name as
876: // button name in the toolbar.
877: var options = this.config[cmd];
878: var k = 0;
879: // btn.element.selectedIndex = 0;
880: for (var j in options) {
881: // FIXME: the following line is scary.
882: if ((j.toLowerCase() == value) ||
883: (options[j].substr(0, value.length).toLowerCase() == value)) {
884: btn.element.selectedIndex = k;
885: break;
886: }
887: ++k;
888: }
889: }
890: break;
891: case "textindicator":
892: if (!text) {
893: try {with (btn.element.style) {
894: backgroundColor = HTMLArea._makeColor(
895: doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor"));
896: if (/transparent/i.test(backgroundColor)) {
897: // Mozilla
898: backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor"));
899: }
900: color = HTMLArea._makeColor(doc.queryCommandValue("forecolor"));
901: fontFamily = doc.queryCommandValue("fontname");
902: fontWeight = doc.queryCommandState("bold") ? "bold" : "normal";
903: fontStyle = doc.queryCommandState("italic") ? "italic" : "normal";
904: }} catch (e) {
905: // alert(e + "\n\n" + cmd);
906: }
907: }
908: break;
909: case "htmlmode": btn.state("active", text); break;
910: default:
911: try {
912: btn.state("active", (!text && doc.queryCommandState(cmd)));
913: } catch (e) {}
914: }
915: }
916: };
917:
918: /** Returns a node after which we can insert other nodes, in the current
919: * selection. The selection is removed. It splits a text node, if needed.
920: */
921: HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) {
922: if (!HTMLArea.is_ie) {
923: var sel = this._getSelection();
924: var range = this._createRange(sel);
925: // remove the current selection
926: sel.removeAllRanges();
927: range.deleteContents();
928: var node = range.startContainer;
929: var pos = range.startOffset;
930: switch (node.nodeType) {
931: case 3: // Node.TEXT_NODE
932: // we have to split it at the caret position.
933: if (toBeInserted.nodeType == 3) {
934: // do optimized insertion
935: node.insertData(pos, toBeInserted.data);
936: range = this._createRange();
937: range.setEnd(node, pos + toBeInserted.length);
938: range.setStart(node, pos + toBeInserted.length);
939: sel.addRange(range);
940: } else {
941: node = node.splitText(pos);
942: var selnode = toBeInserted;
943: if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
944: selnode = selnode.firstChild;
945: }
946: node.parentNode.insertBefore(toBeInserted, node);
947: this.selectNodeContents(selnode);
948: this.updateToolbar();
949: }
950: break;
951: case 1: // Node.ELEMENT_NODE
952: var selnode = toBeInserted;
953: if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
954: selnode = selnode.firstChild;
955: }
956: node.insertBefore(toBeInserted, node.childNodes[pos]);
957: this.selectNodeContents(selnode);
958: this.updateToolbar();
959: break;
960: }
961: } else {
962: return null; // this function not yet used for IE <FIXME>
963: }
964: };
965:
966: // Returns the deepest node that contains both endpoints of the selection.
967: HTMLArea.prototype.getParentElement = function() {
968: var sel = this._getSelection();
969: var range = this._createRange(sel);
970: if (HTMLArea.is_ie) {
971: return range.parentElement ? range.parentElement() : this._doc.body;
972: } else {
973: var p = range.commonAncestorContainer;
974: while (p.nodeType == 3) {
975: p = p.parentNode;
976: }
977: return p;
978: }
979: };
980:
981: // Returns an array with all the ancestor nodes of the selection.
982: HTMLArea.prototype.getAllAncestors = function() {
983: var p = this.getParentElement();
984: var a = [];
985: while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
986: a.push(p);
987: p = p.parentNode;
988: }
989: a.push(this._doc.body);
990: return a;
991: };
992:
993: // Selects the contents inside the given node
994: HTMLArea.prototype.selectNodeContents = function(node, pos) {
995: this.focusEditor();
996: this.forceRedraw();
997: var range;
998: var collapsed = (typeof pos != "undefined");
999: if (HTMLArea.is_ie) {
1000: range = this._doc.body.createTextRange();
1001: range.moveToElementText(node);
1002: (collapsed) && range.collapse(pos);
1003: range.select();
1004: } else {
1005: var sel = this._getSelection();
1006: range = this._doc.createRange();
1007: range.selectNodeContents(node);
1008: (collapsed) && range.collapse(pos);
1009: sel.removeAllRanges();
1010: sel.addRange(range);
1011: }
1012: };
1013:
1014: /** Call this function to insert HTML code at the current position. It deletes
1015: * the selection, if any.
1016: */
1017: HTMLArea.prototype.insertHTML = function(html) {
1018: var sel = this._getSelection();
1019: var range = this._createRange(sel);
1020: if (HTMLArea.is_ie) {
1021: range.pasteHTML(html);
1022: } else {
1023: // construct a new document fragment with the given HTML
1024: var fragment = this._doc.createDocumentFragment();
1025: var div = this._doc.createElement("div");
1026: div.innerHTML = html;
1027: while (div.firstChild) {
1028: // the following call also removes the node from div
1029: fragment.appendChild(div.firstChild);
1030: }
1031: // this also removes the selection
1032: var node = this.insertNodeAtSelection(fragment);
1033: }
1034: };
1035:
1036: /**
1037: * Call this function to surround the existing HTML code in the selection with
1038: * your tags. FIXME: buggy! This function will be deprecated "soon".
1039: */
1040: HTMLArea.prototype.surroundHTML = function(startTag, endTag) {
1041: var html = this.getSelectedHTML();
1042: // the following also deletes the selection
1043: this.insertHTML(startTag + html + endTag);
1044: };
1045:
1046: /// Retrieve the selected block
1047: HTMLArea.prototype.getSelectedHTML = function() {
1048: var sel = this._getSelection();
1049: var range = this._createRange(sel);
1050: var existing = null;
1051: if (HTMLArea.is_ie) {
1052: existing = range.htmlText;
1053: } else {
1054: existing = HTMLArea.getHTML(range.cloneContents(), false);
1055: }
1056: return existing;
1057: };
1058:
1059: // Called when the user clicks on "InsertImage" button
1060: HTMLArea.prototype._insertImage = function() {
1061: var editor = this; // for nested functions
1062: this._popupDialog("insert_image.html", function(param) {
1063: if (!param) { // user must have pressed Cancel
1064: return false;
1065: }
1066: var sel = editor._getSelection();
1067: var range = editor._createRange(sel);
1068: editor._doc.execCommand("insertimage", false, param["f_url"]);
1069: var img = null;
1070: if (HTMLArea.is_ie) {
1071: img = range.parentElement();
1072: // wonder if this works...
1073: if (img.tagName.toLowerCase() != "img") {
1074: img = img.previousSibling;
1075: }
1076: } else {
1077: img = range.startContainer.previousSibling;
1078: }
1079: for (field in param) {
1080: var value = param[field];
1081: if (!value) {
1082: continue;
1083: }
1084: switch (field) {
1085: case "f_alt" : img.alt = value; break;
1086: case "f_border" : img.border = parseInt(value); break;
1087: case "f_align" : img.align = value; break;
1088: case "f_vert" : img.vspace = parseInt(value); break;
1089: case "f_horiz" : img.hspace = parseInt(value); break;
1090: }
1091: }
1092: }, null);
1093: };
1094:
1095: // Called when the user clicks the Insert Table button
1096: HTMLArea.prototype._insertTable = function() {
1097: var sel = this._getSelection();
1098: var range = this._createRange(sel);
1099: var editor = this; // for nested functions
1100: this._popupDialog("insert_table.html", function(param) {
1101: if (!param) { // user must have pressed Cancel
1102: return false;
1103: }
1104: var doc = editor._doc;
1105: // create the table element
1106: var table = doc.createElement("table");
1107: // assign the given arguments
1108: for (var field in param) {
1109: var value = param[field];
1110: if (!value) {
1111: continue;
1112: }
1113: switch (field) {
1114: case "f_width" : table.style.width = value + param["f_unit"]; break;
1115: case "f_align" : table.align = value; break;
1116: case "f_border" : table.border = parseInt(value); break;
1117: case "f_spacing" : table.cellspacing = parseInt(value); break;
1118: case "f_padding" : table.cellpadding = parseInt(value); break;
1119: }
1120: }
1121: var tbody = doc.createElement("tbody");
1122: table.appendChild(tbody);
1123: for (var i = 0; i < param["f_rows"]; ++i) {
1124: var tr = doc.createElement("tr");
1125: tbody.appendChild(tr);
1126: for (var j = 0; j < param["f_cols"]; ++j) {
1127: var td = doc.createElement("td");
1128: tr.appendChild(td);
1129: // Mozilla likes to see something inside the cell.
1130: (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br"));
1131: }
1132: }
1133: if (HTMLArea.is_ie) {
1134: range.pasteHTML(table.outerHTML);
1135: } else {
1136: // insert the table
1137: editor.insertNodeAtSelection(table);
1138: }
1139: return true;
1140: }, null);
1141: };
1142:
1143: /***************************************************
1144: * Category: EVENT HANDLERS
1145: ***************************************************/
1146:
1147: // el is reference to the SELECT object
1148: // txt is the name of the select field, as in config.toolbar
1149: HTMLArea.prototype._comboSelected = function(el, txt) {
1150: this.focusEditor();
1151: var value = el.options[el.selectedIndex].value;
1152: switch (txt) {
1153: case "fontname":
1154: case "fontsize": this.execCommand(txt, false, value); break;
1155: case "formatblock":
1156: (HTMLArea.is_ie) && (value = "<" + value + ">");
1157: this.execCommand(txt, false, value);
1158: break;
1159: default:
1160: // try to look it up in the registered dropdowns
1161: var dropdown = this.config.customSelects[txt];
1162: if (typeof dropdown != "undefined") {
1163: dropdown.action(this);
1164: } else {
1165: alert("FIXME: combo box " + txt + " not implemented");
1166: }
1167: }
1168: };
1169:
1170: // the execCommand function (intercepts some commands and replaces them with
1171: // our own implementation)
1172: HTMLArea.prototype.execCommand = function(cmdID, UI, param) {
1173: var editor = this; // for nested functions
1174: this.focusEditor();
1175: switch (cmdID.toLowerCase()) {
1176: case "htmlmode" : this.setMode(); break;
1177: case "hilitecolor":
1178: (HTMLArea.is_ie) && (cmdID = "backcolor");
1179: case "forecolor":
1180: this._popupDialog("select_color.html", function(color) {
1181: if (color) { // selection not canceled
1182: editor._doc.execCommand(cmdID, false, "#" + color);
1183: }
1184: }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID)));
1185: break;
1186: case "createlink":
1187: if (HTMLArea.is_ie || !UI) {
1188: this._doc.execCommand(cmdID, UI, param);
1189: } else {
1190: // browser is Mozilla & wants UI
1191: var param;
1192: if ((param = prompt("Enter URL"))) {
1193: this._doc.execCommand(cmdID, false, param);
1194: }
1195: }
1196: break;
1197: case "popupeditor":
1198: if (HTMLArea.is_ie) {
1199: window.open(this.popupURL("fullscreen.html"), "ha_fullscreen",
1200: "toolbar=no,location=no,directories=no,status=no,menubar=no," +
1201: "scrollbars=no,resizable=yes,width=640,height=480");
1202: } else {
1203: window.open(this.popupURL("fullscreen.html"), "ha_fullscreen",
1204: "toolbar=no,menubar=no,personalbar=no,width=640,height=480," +
1205: "scrollbars=no,resizable=yes");
1206: }
1207: // pass this object to the newly opened window
1208: HTMLArea._object = this;
1209: break;
1210: case "inserttable": this._insertTable(); break;
1211: case "insertimage": this._insertImage(); break;
1212: case "about" : this._popupDialog("about.html", null, null); break;
1213: case "showhelp" : window.open("reference.html", "ha_help"); break;
1214: default: this._doc.execCommand(cmdID, UI, param);
1215: }
1216: this.updateToolbar();
1217: return false;
1218: };
1219:
1220: /** A generic event handler for things that happen in the IFRAME's document.
1221: * This function also handles key bindings. */
1222: HTMLArea.prototype._editorEvent = function(ev) {
1223: var editor = this;
1224: var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress");
1225: if (keyEvent && ev.ctrlKey) {
1226: var sel = null;
1227: var range = null;
1228: var key = String.fromCharCode(HTMLArea.is_ie ? ev.keyCode : ev.charCode).toLowerCase();
1229: var cmd = null;
1230: var value = null;
1231: switch (key) {
1232: case 'a':
1233: if (!HTMLArea.is_ie) {
1234: // KEY select all
1235: sel = this._getSelection();
1236: sel.removeAllRanges();
1237: range = this._createRange();
1238: range.selectNodeContents(this._doc.body);
1239: sel.addRange(range);
1240: HTMLArea._stopEvent(ev);
1241: }
1242: break;
1243:
1244: // simple key commands follow
1245:
1246: case 'b': cmd = "bold"; break;
1247: case 'i': cmd = "italic"; break;
1248: case 'u': cmd = "underline"; break;
1249: case 's': cmd = "strikethrough"; break;
1250: case 'l': cmd = "justifyleft"; break;
1251: case 'e': cmd = "justifycenter"; break;
1252: case 'r': cmd = "justifyright"; break;
1253: case 'j': cmd = "justifyfull"; break;
1254:
1255: // headings
1256: case '1':
1257: case '2':
1258: case '3':
1259: case '4':
1260: case '5':
1261: case '6':
1262: cmd = "formatblock";
1263: value = "h" + key;
1264: if (HTMLArea.is_ie) {
1265: value = "<" + value + ">";
1266: }
1267: break;
1268: }
1269: if (cmd) {
1270: // execute simple command
1271: this.execCommand(cmd, false, value);
1272: HTMLArea._stopEvent(ev);
1273: }
1274: }
1275: /*
1276: else if (keyEvent) {
1277: // other keys here
1278: switch (ev.keyCode) {
1279: case 13: // KEY enter
1280: // if (HTMLArea.is_ie) {
1281: this.insertHTML("<br />");
1282: HTMLArea._stopEvent(ev);
1283: // }
1284: break;
1285: }
1286: }
1287: */
1288: // update the toolbar state after some time
1289: if (editor._timerToolbar) {
1290: clearTimeout(editor._timerToolbar);
1291: }
1292: editor._timerToolbar = setTimeout(function() {
1293: editor.updateToolbar();
1294: editor._timerToolbar = null;
1295: }, 50);
1296: };
1297:
1298: // retrieve the HTML
1299: HTMLArea.prototype.getHTML = function() {
1300: switch (this._editMode) {
1301: case "wysiwyg" : return HTMLArea.getHTML(this._doc.body, false);
1302: case "textmode" : return this._textArea.value;
1303: default : alert("Mode <" + mode + "> not defined!");
1304: }
1305: return false;
1306: };
1307:
1308: // retrieve the HTML (fastest version, but uses innerHTML)
1309: HTMLArea.prototype.getInnerHTML = function() {
1310: switch (this._editMode) {
1311: case "wysiwyg" : return this._doc.body.innerHTML;
1312: case "textmode" : return this._textArea.value;
1313: default : alert("Mode <" + mode + "> not defined!");
1314: }
1315: return false;
1316: };
1317:
1318: // completely change the HTML inside
1319: HTMLArea.prototype.setHTML = function(html) {
1320: switch (this._editMode) {
1321: case "wysiwyg" : this._doc.body.innerHTML = html; break;
1322: case "textmode" : this._textArea.value = html; break;
1323: default : alert("Mode <" + mode + "> not defined!");
1324: }
1325: return false;
1326: };
1327:
1328: /***************************************************
1329: * Category: UTILITY FUNCTIONS
1330: ***************************************************/
1331:
1332: // browser identification
1333:
1334: HTMLArea.agt = navigator.userAgent.toLowerCase();
1335: HTMLArea.is_ie = ((HTMLArea.agt.indexOf("msie") != -1) && (HTMLArea.agt.indexOf("opera") == -1));
1336: HTMLArea.is_opera = (HTMLArea.agt.indexOf("opera") != -1);
1337: HTMLArea.is_mac = (HTMLArea.agt.indexOf("mac") != -1);
1338: HTMLArea.is_mac_ie = (HTMLArea.is_ie && HTMLArea.is_mac);
1339: HTMLArea.is_win_ie = (HTMLArea.is_ie && !HTMLArea.is_mac);
1340: HTMLArea.is_gecko = (navigator.product == "Gecko");
1341:
1342: // variable used to pass the object to the popup editor window.
1343: HTMLArea._object = null;
1344:
1345: // FIXME!!! this should return false for IE < 5.5
1346: HTMLArea.checkSupportedBrowser = function() {
1347: if (HTMLArea.is_gecko) {
1348: if (navigator.productSub < 20021201) {
1.2 ! www 1349: window.status="You need at least Mozilla-1.3 Alpha. " +
! 1350: "Sorry, your Gecko is not supported.";
1.1 www 1351: return false;
1352: }
1353: if (navigator.productSub < 20030210) {
1.2 ! www 1354: window.status="Mozilla < 1.3 Beta is not supported! " +
! 1355: "I'll try, though, but it might not work.";
1.1 www 1356: }
1357: }
1358: return HTMLArea.is_gecko || HTMLArea.is_ie;
1359: };
1360:
1361: // selection & ranges
1362:
1363: // returns the current selection object
1364: HTMLArea.prototype._getSelection = function() {
1365: if (HTMLArea.is_ie) {
1366: return this._doc.selection;
1367: } else {
1368: return this._iframe.contentWindow.getSelection();
1369: }
1370: };
1371:
1372: // returns a range for the current selection
1373: HTMLArea.prototype._createRange = function(sel) {
1374: if (HTMLArea.is_ie) {
1375: return sel.createRange();
1376: } else {
1377: this.focusEditor();
1378: if (typeof sel != "undefined") {
1379: return sel.getRangeAt(0);
1380: } else {
1381: return this._doc.createRange();
1382: }
1383: }
1384: };
1385:
1386: // event handling
1387:
1388: HTMLArea._addEvent = function(el, evname, func) {
1389: if (HTMLArea.is_ie) {
1390: el.attachEvent("on" + evname, func);
1391: } else {
1392: el.addEventListener(evname, func, true);
1393: }
1394: };
1395:
1396: HTMLArea._addEvents = function(el, evs, func) {
1397: for (var i in evs) {
1398: HTMLArea._addEvent(el, evs[i], func);
1399: }
1400: };
1401:
1402: HTMLArea._removeEvent = function(el, evname, func) {
1403: if (HTMLArea.is_ie) {
1404: el.detachEvent("on" + evname, func);
1405: } else {
1406: el.removeEventListener(evname, func, true);
1407: }
1408: };
1409:
1410: HTMLArea._removeEvents = function(el, evs, func) {
1411: for (var i in evs) {
1412: HTMLArea._removeEvent(el, evs[i], func);
1413: }
1414: };
1415:
1416: HTMLArea._stopEvent = function(ev) {
1417: if (HTMLArea.is_ie) {
1418: ev.cancelBubble = true;
1419: ev.returnValue = false;
1420: } else {
1421: ev.preventDefault();
1422: ev.stopPropagation();
1423: }
1424: };
1425:
1426: HTMLArea._removeClass = function(el, className) {
1427: if (!(el && el.className)) {
1428: return;
1429: }
1430: var cls = el.className.split(" ");
1431: var ar = new Array();
1432: for (var i = cls.length; i > 0;) {
1433: if (cls[--i] != className) {
1434: ar[ar.length] = cls[i];
1435: }
1436: }
1437: el.className = ar.join(" ");
1438: };
1439:
1440: HTMLArea._addClass = function(el, className) {
1441: // remove the class first, if already there
1442: HTMLArea._removeClass(el, className);
1443: el.className += " " + className;
1444: };
1445:
1446: HTMLArea._hasClass = function(el, className) {
1447: if (!(el && el.className)) {
1448: return false;
1449: }
1450: var cls = el.className.split(" ");
1451: for (var i = cls.length; i > 0;) {
1452: if (cls[--i] == className) {
1453: return true;
1454: }
1455: }
1456: return false;
1457: };
1458:
1459: HTMLArea.isBlockElement = function(el) {
1460: var blockTags = " body form textarea fieldset ul ol dl li div " +
1461: "p h1 h2 h3 h4 h5 h6 quote pre table thead " +
1462: "tbody tfoot tr td iframe address ";
1463: return (blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
1464: };
1465:
1466: HTMLArea.needsClosingTag = function(el) {
1467: var closingTags = " script style div span tr td tbody table em strong font a ";
1468: return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
1469: };
1470:
1471: // performs HTML encoding of some given string
1472: HTMLArea.htmlEncode = function(str) {
1473: // we don't need regexp for that, but.. so be it for now.
1474: str = str.replace(/&/ig, "&");
1475: str = str.replace(/</ig, "<");
1476: str = str.replace(/>/ig, ">");
1477: str = str.replace(/\x22/ig, """);
1478: // \x22 means '"' -- we use hex reprezentation so that we don't disturb
1479: // JS compressors (well, at least mine fails.. ;)
1480: return str;
1481: };
1482:
1483: // Retrieves the HTML code from the given node. This is a replacement for
1484: // getting innerHTML, using standard DOM calls.
1485: HTMLArea.getHTML = function(root, outputRoot) {
1486: var html = "";
1487: switch (root.nodeType) {
1488: case 1: // Node.ELEMENT_NODE
1489: case 11: // Node.DOCUMENT_FRAGMENT_NODE
1490: var closed;
1491: var i;
1492: if (outputRoot) {
1493: closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root)));
1494: html = "<" + root.tagName.toLowerCase();
1495: var attrs = root.attributes;
1496: for (i = 0; i < attrs.length; ++i) {
1497: var a = attrs.item(i);
1498: if (!a.specified) {
1499: continue;
1500: }
1501: var name = a.nodeName.toLowerCase();
1502: if (/_moz/.test(name)) {
1503: // Mozilla reports some special tags
1504: // here; we don't need them.
1505: continue;
1506: }
1507: var value;
1508: if (name != "style") {
1509: // IE5.5 reports 25 when cellSpacing is
1510: // 1; other values might be doomed too.
1511: // For this reason we extract the
1512: // values directly from the root node.
1513: // I'm starting to HATE JavaScript
1514: // development. Browser differences
1515: // suck.
1516: if (typeof root[a.nodeName] != "undefined") {
1517: value = root[a.nodeName];
1518: } else {
1519: value = a.nodeValue;
1520: }
1521: } else { // IE fails to put style in attributes list
1522: // FIXME: cssText reported by IE is UPPERCASE
1523: value = root.style.cssText;
1524: }
1525: if (/_moz/.test(value)) {
1526: // Mozilla reports some special tags
1527: // here; we don't need them.
1528: continue;
1529: }
1530: html += " " + name + '="' + value + '"';
1531: }
1532: html += closed ? " />" : ">";
1533: }
1534: for (i = root.firstChild; i; i = i.nextSibling) {
1535: html += HTMLArea.getHTML(i, true);
1536: }
1537: if (outputRoot && !closed) {
1538: html += "</" + root.tagName.toLowerCase() + ">";
1539: }
1540: break;
1541: case 3: // Node.TEXT_NODE
1542: html = HTMLArea.htmlEncode(root.data);
1543: break;
1544: case 8: // Node.COMMENT_NODE
1545: html = "<!--" + root.data + "-->";
1546: break; // skip comments, for now.
1547: }
1548: return html;
1549: };
1550:
1551: // creates a rgb-style color from a number
1552: HTMLArea._makeColor = function(v) {
1553: if (typeof v != "number") {
1554: // already in rgb (hopefully); IE doesn't get here.
1555: return v;
1556: }
1557: // IE sends number; convert to rgb.
1558: var r = v & 0xFF;
1559: var g = (v >> 8) & 0xFF;
1560: var b = (v >> 16) & 0xFF;
1561: return "rgb(" + r + "," + g + "," + b + ")";
1562: };
1563:
1564: // returns hexadecimal color representation from a number or a rgb-style color.
1565: HTMLArea._colorToRgb = function(v) {
1566: // returns the hex representation of one byte (2 digits)
1567: function hex(d) {
1568: return (d < 16) ? ("0" + d.toString(16)) : d.toString(16);
1569: };
1570:
1571: if (typeof v == "number") {
1572: // we're talking to IE here
1573: var r = v & 0xFF;
1574: var g = (v >> 8) & 0xFF;
1575: var b = (v >> 16) & 0xFF;
1576: return "#" + hex(r) + hex(g) + hex(b);
1577: }
1578:
1579: if (v.substr(0, 3) == "rgb") {
1580: // in rgb(...) form -- Mozilla
1581: var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/;
1582: if (v.match(re)) {
1583: var r = parseInt(RegExp.$1);
1584: var g = parseInt(RegExp.$2);
1585: var b = parseInt(RegExp.$3);
1586: return "#" + hex(r) + hex(g) + hex(b);
1587: }
1588: // doesn't match RE?! maybe uses percentages or float numbers
1589: // -- FIXME: not yet implemented.
1590: return null;
1591: }
1592:
1593: if (v[0] == "#") {
1594: // already hex rgb (hopefully :D )
1595: return v;
1596: }
1597:
1598: // if everything else fails ;)
1599: return null;
1600: };
1601:
1602: // modal dialogs for Mozilla (for IE we're using the showModalDialog() call).
1603:
1604: // receives an URL to the popup dialog and a function that receives one value;
1605: // this function will get called after the dialog is closed, with the return
1606: // value of the dialog.
1607: HTMLArea.prototype._popupDialog = function(url, action, init) {
1608: Dialog(this.popupURL(url), action, init);
1609: };
1610:
1611: // paths
1612:
1613: HTMLArea.prototype.imgURL = function(file, plugin) {
1614: if (typeof plugin == "undefined") {
1615: return this.config.editorURL + file;
1616: } else {
1617: return this.config.editorURL + "plugins/" + plugin + "/img/" + file;
1618: }
1619: };
1620:
1621: HTMLArea.prototype.popupURL = function(file) {
1622: return this.config.editorURL + this.config.popupURL + file;
1623: };
1624:
1625: // EOF
1626: // Local variables: //
1627: // c-basic-offset:8 //
1628: // indent-tabs-mode:t //
1629: // End: //
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>