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