Merge commit 'catalyst/MOODLE_19_STABLE' into mdl19-linuxchix
[moodle-linuxchix.git] / lib / editor / htmlarea / htmlarea.php
blob87d4122317e208b8d231f2febdc679d3614ef66a
1 <?php
2 include("../../../config.php");
3 require_once($CFG->dirroot.'/lib/languages.php');
5 $id = optional_param('id', SITEID, PARAM_INT);
6 $httpsrequired = optional_param('httpsrequired', 0, PARAM_BOOL); //flag indicating editor on page with required https
8 require_course_login($id);
10 $lastmodified = filemtime("htmlarea.php");
11 $lifetime = 1800;
13 // Commenting this out since it's creating problems
14 // where solution seem to be hard to find...
15 // http://moodle.org/mod/forum/discuss.php?d=34376
16 //if ( function_exists('ob_gzhandler') ) {
17 // ob_start("ob_gzhandler");
18 //}
20 header("Content-type: application/x-javascript; charset: utf-8"); // Correct MIME type
21 header("Last-Modified: " . gmdate("D, d M Y H:i:s", $lastmodified) . " GMT");
22 header("Expires: " . gmdate("D, d M Y H:i:s", time() + $lifetime) . " GMT");
23 header("Cache-control: max_age = $lifetime");
24 header("Pragma: ");
26 $lang = current_language();
28 if (empty($lang)) {
29 $lang = "en";
32 if ($httpsrequired) {
33 // this is an ugly hack to allow partial operation of editor on pages that require https when loginhttps enabled
34 // please note that some popups still show nonsecurre items and fullscreen may not function properly in IE
35 $url = preg_replace('|https?://[^/]+|', '', $CFG->wwwroot).'/lib/editor/htmlarea/';
36 } else {
37 $url = $CFG->wwwroot.'/lib/editor/htmlarea/';
40 $strheading = get_string("heading", "editor");
41 $strnormal = get_string("normal", "editor");
42 $straddress = get_string("address", "editor");
43 $strpreformatted = get_string("preformatted", "editor");
44 $strlang = get_string('lang', 'editor');
45 $strmulti = get_string('multi', 'editor');
48 // htmlArea v3.0 - Copyright (c) 2002, 2003 interactivetools.com, inc.
49 // This copyright notice MUST stay intact for use (see license.txt).
51 // Portions (c) dynarch.com, 2003-2004
53 // A free WYSIWYG editor replacement for <textarea> fields.
54 // For full source code and docs, visit http://www.interactivetools.com/
56 // Version 3.0 developed by Mihai Bazon.
57 // http://dynarch.com/mishoo
59 // $Id$
61 if (typeof _editor_url == "string") {
62 // Leave exactly one backslash at the end of _editor_url
63 _editor_url = _editor_url.replace(/\x2f*$/, '/');
64 } else {
65 //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.");
66 _editor_url = '<?php echo $url; ?>';// we need relative path to site root for editor in pages wit hrequired https
69 // make sure we have a language
70 if (typeof _editor_lang == "string") {
71 _editor_lang = "en"; // should always be english in moodle.
72 } else {
73 _editor_lang = "en";
76 // Creates a new HTMLArea object. Tries to replace the textarea with the given
77 // ID with it.
78 function HTMLArea(textarea, config) {
79 if (HTMLArea.checkSupportedBrowser()) {
80 if (typeof config == "undefined") {
81 this.config = new HTMLArea.Config();
82 } else {
83 this.config = config;
85 this._htmlArea = null;
86 this._textArea = textarea;
87 this._editMode = "wysiwyg";
88 this.plugins = {};
89 this._timerToolbar = null;
90 this._timerUndo = null;
91 this._undoQueue = new Array(this.config.undoSteps);
92 this._undoPos = -1;
93 this._customUndo = true;
94 this._mdoc = document; // cache the document, we need it in plugins
95 this.doctype = '';
96 this.dropdowns = []; // Array of select elements in the toolbar
100 // load some scripts
101 (function() {
102 var scripts = HTMLArea._scripts = [ _editor_url + "htmlarea.js",
103 _editor_url + "dialog.js",
104 _editor_url + "popupwin.js" ];
105 var head = document.getElementsByTagName("head")[0];
106 // start from 1, htmlarea.js is already loaded
107 for (var i = 1; i < scripts.length; ++i) {
108 var script = document.createElement("script");
109 script.src = scripts[i];
110 head.appendChild(script);
112 })();
114 // cache some regexps
115 HTMLArea.RE_tagName = /(<\/|<)\s*([^ \t\n>]+)/ig;
116 HTMLArea.RE_doctype = /(<!doctype((.|\n)*?)>)\n?/i;
117 HTMLArea.RE_head = /<head>((.|\n)*?)<\/head>/i;
118 HTMLArea.RE_body = /<body>((.|\n)*?)<\/body>/i;
119 HTMLArea.RE_blocktag = /^(h1|h2|h3|h4|h5|h6|p|address|pre)$/i;
120 HTMLArea.RE_junktag = /^\/($|\/)/;
121 // Hopefully a complete list of tags that MSIEs parser will consider
122 // as possible content tags. Retrieved from
123 // http://www.echoecho.com/htmlreference.htm
124 HTMLArea.RE_msietag = /^\/?(a|abbr|acronym|address|applet|area|b|base|basefont|bdo|bgsound|big|blink|blockquote|body|br|button|caption|center|cite|code|col|colgroup|comment|dd|del|dfn|dir|div|dl|dt|em|embed|fieldset|font|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|hr|html|i|iframe|ilayer|img|input|ins|isindex|kbd|keygen|label|layer|legend|li|link|map|marquee|menu|meta|multicol|nobr|noembed|noframes|nolayer|noscript|object|ol|optgroup|option|p|param|plaintext|pre|q|s|samp|script|select|server|small|spacer|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|title|tr|tt|u|ul|var)$/i
126 HTMLArea.Config = function () {
127 this.version = "3.0";
129 this.width = "auto";
130 this.height = "auto";
132 // enable creation of a status bar?
133 this.statusBar = true;
135 // maximum size of the undo queue
136 this.undoSteps = 20;
138 // the time interval at which undo samples are taken
139 this.undoTimeout = 500; // 1/2 sec.
141 // the next parameter specifies whether the toolbar should be included
142 // in the size or not.
143 this.sizeIncludesToolbar = true;
145 // if true then HTMLArea will retrieve the full HTML, starting with the
146 // <HTML> tag.
147 this.fullPage = false;
149 // style included in the iframe document
150 this.pageStyle = "body { background-color: #fff; font-family: 'Times New Roman', Times; } \n .lang { background-color: #dee; }";
152 // set to true if you want Word code to be cleaned upon Paste
153 this.killWordOnPaste = true;
155 // BaseURL included in the iframe document
156 this.baseURL = document.baseURI || document.URL;
157 if (this.baseURL && this.baseURL.match(/(.*)\/([^\/]+)/))
158 this.baseURL = RegExp.$1 + "/";
160 // URL-s
161 this.imgURL = "images/";
162 this.popupURL = "popups/";
164 this.toolbar = [
165 [ "fontname", "space",
166 "fontsize", "space",
167 "formatblock", "space",
168 "language", "space",
169 "bold", "italic", "underline", "strikethrough", "separator",
170 "subscript", "superscript", "separator",
171 "clean", "separator", "undo", "redo" ],
173 [ "justifyleft", "justifycenter", "justifyright", "justifyfull", "separator",
174 "lefttoright", "righttoleft", "separator",
175 "insertorderedlist", "insertunorderedlist", "outdent", "indent", "separator",
176 "forecolor", "hilitecolor", "separator",
177 "inserthorizontalrule", "createanchor", "createlink", "unlink", "nolink", "separator",
178 "insertimage", "inserttable",
179 "insertsmile", "insertchar", "search_replace",
180 <?php if (!empty($CFG->aspellpath) && file_exists($CFG->aspellpath) && !empty($CFG->editorspelling)) {
181 echo '"separator","spellcheck",';
182 } ?>
183 "separator", "htmlmode", "separator", "popupeditor"]
186 this.fontname = {
187 "Arial": 'arial,helvetica,sans-serif',
188 "Courier New": 'courier new,courier,monospace',
189 "Georgia": 'georgia,times new roman,times,serif',
190 "Tahoma": 'tahoma,arial,helvetica,sans-serif',
191 "Times New Roman": 'times new roman,times,serif',
192 "Verdana": 'verdana,arial,helvetica,sans-serif',
193 "Impact": 'impact',
194 "WingDings": 'wingdings'
197 this.fontsize = {
198 "1 (8 pt)": "1",
199 "2 (10 pt)": "2",
200 "3 (12 pt)": "3",
201 "4 (14 pt)": "4",
202 "5 (18 pt)": "5",
203 "6 (24 pt)": "6",
204 "7 (36 pt)": "7"
207 this.formatblock = {
208 "":"",
209 "<?php echo $strheading ?> 1": "h1",
210 "<?php echo $strheading ?> 2": "h2",
211 "<?php echo $strheading ?> 3": "h3",
212 "<?php echo $strheading ?> 4": "h4",
213 "<?php echo $strheading ?> 5": "h5",
214 "<?php echo $strheading ?> 6": "h6",
215 "<?php echo $strnormal ?>": "p",
216 "<?php echo $straddress ?>": "address",
217 "<?php echo $strpreformatted ?>": "pre"
220 this.language = {
221 "<?php echo $strlang; ?>":"",
222 <?php
223 $strlangarray = '';
224 foreach ($LANGUAGES as $key => $name) {
225 $key = str_replace('_', '-', $key);
226 $strlangarray .= '"'.$key.'": "'.$key.'",';
228 $strlangarray .= '"'.$strmulti.'": "multi",';
230 foreach ($LANGUAGES as $key => $name) {
231 $key = str_replace('_', '-', $key);
232 $strlangarray .= '"'.$key.' ": "'.$key.'_ML",';
234 $strlangarray = substr($strlangarray, 0, -1);
235 echo $strlangarray;
239 this.customSelects = {};
241 function cut_copy_paste(e, cmd, obj) {
242 e.execCommand(cmd);
245 this.btnList = {
246 bold: [ "Bold", "ed_format_bold.gif", false, function(e) {e.execCommand("bold");} ],
247 italic: [ "Italic", "ed_format_italic.gif", false, function(e) {e.execCommand("italic");} ],
248 underline: [ "Underline", "ed_format_underline.gif", false, function(e) {e.execCommand("underline");} ],
249 strikethrough: [ "Strikethrough", "ed_format_strike.gif", false, function(e) {e.execCommand("strikethrough");} ],
250 subscript: [ "Subscript", "ed_format_sub.gif", false, function(e) {e.execCommand("subscript");} ],
251 superscript: [ "Superscript", "ed_format_sup.gif", false, function(e) {e.execCommand("superscript");} ],
252 justifyleft: [ "Justify Left", "ed_align_left.gif", false, function(e) {e.execCommand("justifyleft");} ],
253 justifycenter: [ "Justify Center", "ed_align_center.gif", false, function(e) {e.execCommand("justifycenter");} ],
254 justifyright: [ "Justify Right", "ed_align_right.gif", false, function(e) {e.execCommand("justifyright");} ],
255 justifyfull: [ "Justify Full", "ed_align_justify.gif", false, function(e) {e.execCommand("justifyfull");} ],
256 insertorderedlist: [ "Ordered List", "ed_list_num.gif", false, function(e) {e.execCommand("insertorderedlist");} ],
257 insertunorderedlist: [ "Bulleted List", "ed_list_bullet.gif", false, function(e) {e.execCommand("insertunorderedlist");} ],
258 outdent: [ "Decrease Indent", "ed_indent_less.gif", false, function(e) {e.execCommand("outdent");} ],
259 indent: [ "Increase Indent", "ed_indent_more.gif", false, function(e) {e.execCommand("indent");} ],
260 forecolor: [ "Font Color", "ed_color_fg.gif", false, function(e) {e.execCommand("forecolor");} ],
261 hilitecolor: [ "Background Color", "ed_color_bg.gif", false, function(e) {e.execCommand("hilitecolor");} ],
262 inserthorizontalrule: [ "Horizontal Rule", "ed_hr.gif", false, function(e) {e.execCommand("inserthorizontalrule");} ],
263 createanchor: [ "Create anchor", "ed_anchor.gif", false, function(e) {e.execCommand("createanchor", true);} ],
264 createlink: [ "Insert Web Link", "ed_link.gif", false, function(e) {e.execCommand("createlink", true);} ],
265 unlink: [ "Remove Link", "ed_unlink.gif", false, function(e) {e.execCommand("unlink");} ],
266 nolink: [ "No link", "ed_nolink.gif", false, function(e) {e.execCommand("nolink");} ],
267 insertimage: [ "Insert/Modify Image", "ed_image.gif", false, function(e) {e.execCommand("insertimage");} ],
268 inserttable: [ "Insert Table", "insert_table.gif", false, function(e) {e.execCommand("inserttable");} ],
269 htmlmode: [ "Toggle HTML Source", "ed_html.gif", true, function(e) {e.execCommand("htmlmode");} ],
270 popupeditor: [ "Enlarge Editor", "fullscreen_maximize.gif", true, function(e) {e.execCommand("popupeditor");} ],
271 about: [ "About this editor", "ed_about.gif", true, function(e) {e.execCommand("about");} ],
272 showhelp: [ "Help using editor", "ed_help.gif", true, function(e) {e.execCommand("showhelp");} ],
273 undo: [ "Undoes your last action", "ed_undo.gif", false, function(e) {e.execCommand("undo");} ],
274 redo: [ "Redoes your last action", "ed_redo.gif", false, function(e) {e.execCommand("redo");} ],
275 clean: [ "Clean Word HTML", "ed_wordclean.gif", false, function(e) {e.execCommand("killword"); }],
276 lefttoright: [ "Direction left to right", "ed_left_to_right.gif", false, function(e) {e.execCommand("lefttoright");} ],
277 righttoleft: [ "Direction right to left", "ed_right_to_left.gif", false, function(e) {e.execCommand("righttoleft");} ],
278 <?php if (!empty($CFG->aspellpath) && file_exists($CFG->aspellpath) && !empty($CFG->editorspelling)) {
279 echo 'spellcheck: ["Spell-check", "spell-check.gif", false, spellClickHandler ],'."\n";
281 insertsmile: ["Insert Smiley", "em.icon.smile.gif", false, function(e) {e.execCommand("insertsmile");} ],
282 insertchar: [ "Insert Char", "icon_ins_char.gif", false, function(e) {e.execCommand("insertchar");} ],
283 search_replace: [ "Search and replace", "ed_replace.gif", false, function(e) {e.execCommand("searchandreplace");} ]
286 // initialize tooltips from the I18N module and generate correct image path
287 for (var i in this.btnList) {
288 var btn = this.btnList[i];
289 btn[1] = _editor_url + this.imgURL + btn[1];
290 if (typeof HTMLArea.I18N.tooltips[i] != "undefined") {
291 btn[0] = HTMLArea.I18N.tooltips[i];
296 HTMLArea.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context) {
297 var the_id;
298 if (typeof id == "string") {
299 the_id = id;
300 } else if (typeof id == "object") {
301 the_id = id.id;
302 } else {
303 alert("ERROR [HTMLArea.Config::registerButton]:\ninvalid arguments");
304 return false;
306 // check for existing id
307 if (typeof this.customSelects[the_id] != "undefined") {
308 // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
310 if (typeof this.btnList[the_id] != "undefined") {
311 // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
313 switch (typeof id) {
314 case "string": this.btnList[id] = [ tooltip, image, textMode, action, context ]; break;
315 case "object": this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; break;
319 HTMLArea.Config.prototype.registerDropdown = function(object) {
320 // check for existing id
321 if (typeof this.customSelects[object.id] != "undefined") {
322 // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
324 if (typeof this.btnList[object.id] != "undefined") {
325 // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
327 this.customSelects[object.id] = object;
330 HTMLArea.Config.prototype.hideSomeButtons = function(remove) {
331 var toolbar = this.toolbar;
332 for (var i in toolbar) {
333 var line = toolbar[i];
334 for (var j = line.length; --j >= 0; ) {
335 if (remove.indexOf(" " + line[j] + " ") >= 0) {
336 var len = 1;
337 if (/separator|space/.test(line[j + 1])) {
338 len = 2;
340 line.splice(j, len);
346 /** Helper function: replace all TEXTAREA-s in the document with HTMLArea-s. */
347 HTMLArea.replaceAll = function(config) {
348 var tas = document.getElementsByTagName("textarea");
349 for (var i = tas.length; i > 0; (new HTMLArea(tas[--i], config)).generate());
352 /** Helper function: replaces the TEXTAREA with the given ID with HTMLArea. */
353 HTMLArea.replace = function(id, config) {
354 var ta = HTMLArea.getElementById("textarea", id);
355 return ta ? (new HTMLArea(ta, config)).generate() : null;
358 // Creates the toolbar and appends it to the _htmlarea
359 HTMLArea.prototype._createToolbar = function () {
360 var editor = this; // to access this in nested functions
362 var toolbar = document.createElement("div");
363 this._toolbar = toolbar;
364 toolbar.className = "toolbar";
365 toolbar.unselectable = "1";
366 var tb_row = null;
367 var tb_objects = new Object();
368 this._toolbarObjects = tb_objects;
370 // creates a new line in the toolbar
371 function newLine() {
372 var table = document.createElement("table");
373 table.border = "0px";
374 table.cellSpacing = "0px";
375 table.cellPadding = "0px";
376 toolbar.appendChild(table);
377 // TBODY is required for IE, otherwise you don't see anything
378 // in the TABLE.
379 var tb_body = document.createElement("tbody");
380 table.appendChild(tb_body);
381 tb_row = document.createElement("tr");
382 tb_body.appendChild(tb_row);
383 }; // END of function: newLine
384 // init first line
385 newLine();
387 function setButtonStatus(id, newval) {
388 var oldval = this[id];
389 var el = this.element;
390 if (oldval != newval) {
391 switch (id) {
392 case "enabled":
393 if (newval) {
394 HTMLArea._removeClass(el, "buttonDisabled");
395 el.disabled = false;
396 } else {
397 HTMLArea._addClass(el, "buttonDisabled");
398 el.disabled = true;
400 break;
401 case "active":
402 if (newval) {
403 HTMLArea._addClass(el, "buttonPressed");
404 } else {
405 HTMLArea._removeClass(el, "buttonPressed");
407 break;
409 this[id] = newval;
411 }; // END of function: setButtonStatus
413 function createSelect(txt) {
414 var options = null;
415 var el = null;
416 var cmd = null;
417 var customSelects = editor.config.customSelects;
418 var context = null;
419 switch (txt) {
420 case "fontsize":
421 case "fontname":
422 case "formatblock":
423 case "language":
424 options = editor.config[txt];
425 cmd = txt;
426 break;
427 default:
428 // try to fetch it from the list of registered selects
429 cmd = txt;
430 var dropdown = customSelects[cmd];
431 if (typeof dropdown != "undefined") {
432 options = dropdown.options;
433 context = dropdown.context;
434 } else {
435 alert("ERROR [createSelect]:\nCan't find the requested dropdown definition");
437 break;
439 if (options) {
440 el = document.createElement("select");
441 var obj = {
442 name : txt, // field name
443 element : el, // the UI element (SELECT)
444 enabled : true, // is it enabled?
445 text : false, // enabled in text mode?
446 cmd : cmd, // command ID
447 state : setButtonStatus, // for changing state
448 context : context
450 tb_objects[txt] = obj;
451 for (var i in options) {
452 var op = document.createElement("option");
453 op.appendChild(document.createTextNode(i));
454 op.value = options[i];
455 el.appendChild(op);
457 HTMLArea._addEvent(el, "change", function () {
458 editor._comboSelected(el, txt);
461 editor.dropdowns[txt] = el; // Keep track of the element for keyboard
462 // access later.
463 return el;
464 }; // END of function: createSelect
466 // appends a new button to toolbar
467 function createButton(txt) {
468 // the element that will be created
469 var el = null;
470 var btn = null;
471 switch (txt) {
472 case "separator":
473 el = document.createElement("div");
474 el.className = "separator";
475 break;
476 case "space":
477 el = document.createElement("div");
478 el.className = "space";
479 break;
480 case "linebreak":
481 newLine();
482 return false;
483 case "textindicator":
484 el = document.createElement("div");
485 el.appendChild(document.createTextNode("A"));
486 el.className = "indicator";
487 el.title = HTMLArea.I18N.tooltips.textindicator;
488 var obj = {
489 name : txt, // the button name (i.e. 'bold')
490 element : el, // the UI element (DIV)
491 enabled : true, // is it enabled?
492 active : false, // is it pressed?
493 text : false, // enabled in text mode?
494 cmd : "textindicator", // the command ID
495 state : setButtonStatus // for changing state
497 tb_objects[txt] = obj;
498 break;
499 default:
500 btn = editor.config.btnList[txt];
502 if (!el && btn) {
503 el = document.createElement("div");
504 el.title = btn[0];
505 el.className = "button";
506 // let's just pretend we have a button object, and
507 // assign all the needed information to it.
508 var obj = {
509 name : txt, // the button name (i.e. 'bold')
510 element : el, // the UI element (DIV)
511 enabled : true, // is it enabled?
512 active : false, // is it pressed?
513 text : btn[2], // enabled in text mode?
514 cmd : btn[3], // the command ID
515 state : setButtonStatus, // for changing state
516 context : btn[4] || null // enabled in a certain context?
518 tb_objects[txt] = obj;
519 // handlers to emulate nice flat toolbar buttons
520 HTMLArea._addEvent(el, "mouseover", function () {
521 if (obj.enabled) {
522 HTMLArea._addClass(el, "buttonHover");
525 HTMLArea._addEvent(el, "mouseout", function () {
526 if (obj.enabled) with (HTMLArea) {
527 _removeClass(el, "buttonHover");
528 _removeClass(el, "buttonActive");
529 (obj.active) && _addClass(el, "buttonPressed");
532 HTMLArea._addEvent(el, "mousedown", function (ev) {
533 if (obj.enabled) with (HTMLArea) {
534 _addClass(el, "buttonActive");
535 _removeClass(el, "buttonPressed");
536 _stopEvent(is_ie ? window.event : ev);
539 // when clicked, do the following:
540 HTMLArea._addEvent(el, "click", function (ev) {
541 if (obj.enabled) with (HTMLArea) {
542 _removeClass(el, "buttonActive");
543 _removeClass(el, "buttonHover");
544 obj.cmd(editor, obj.name, obj);
545 _stopEvent(is_ie ? window.event : ev);
548 var img = document.createElement("img");
549 img.src = btn[1];
550 img.style.width = "18px";
551 img.style.height = "18px";
552 el.appendChild(img);
553 } else if (!el) {
554 el = createSelect(txt);
556 if (el) {
557 var tb_cell = document.createElement("td");
558 tb_row.appendChild(tb_cell);
559 tb_cell.appendChild(el);
560 } else {
561 alert("FIXME: Unknown toolbar item: " + txt);
563 return el;
566 var first = true;
567 for (var i in this.config.toolbar) {
568 if (this.config.toolbar.propertyIsEnumerable(i)) { // fix for prototype.js compatibility
569 if (!first) {
570 createButton("linebreak");
571 } else {
572 first = false;
574 var group = this.config.toolbar[i];
575 for (var j in group) {
576 if (group.propertyIsEnumerable(j)) { // fix for prototype.js compatibility
577 var code = group[j];
578 if (/^([IT])\[(.*?)\]/.test(code)) {
579 // special case, create text label
580 var l7ed = RegExp.$1 == "I"; // localized?
581 var label = RegExp.$2;
582 if (l7ed) {
583 label = HTMLArea.I18N.custom[label];
585 var tb_cell = document.createElement("td");
586 tb_row.appendChild(tb_cell);
587 tb_cell.className = "label";
588 tb_cell.innerHTML = label;
589 } else {
590 createButton(code);
597 this._htmlArea.appendChild(toolbar);
600 HTMLArea.prototype._createStatusBar = function() {
601 var statusbar = document.createElement("div");
602 statusbar.className = "statusBar";
603 this._htmlArea.appendChild(statusbar);
604 this._statusBar = statusbar;
605 // statusbar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
606 // creates a holder for the path view
607 div = document.createElement("span");
608 div.className = "statusBarTree";
609 div.innerHTML = HTMLArea.I18N.msg["Path"] + ": ";
610 this._statusBarTree = div;
611 this._statusBar.appendChild(div);
612 if (!this.config.statusBar) {
613 // disable it...
614 statusbar.style.display = "none";
618 // Creates the HTMLArea object and replaces the textarea with it.
619 HTMLArea.prototype.generate = function () {
620 var editor = this; // we'll need "this" in some nested functions
622 // get the textarea
623 var textarea = this._textArea;
624 if (typeof textarea == "string") {
625 // it's not element but ID
626 this._textArea = textarea = HTMLArea.getElementById("textarea", textarea);
628 // Fix for IE's sticky bug. Editor doesn't load
629 // editing area.
630 var height;
631 if ( textarea.offsetHeight && textarea.offsetHeight > 0 ) {
632 height = textarea.offsetHeight;
633 } else {
634 height = 300;
636 this._ta_size = {
637 w: textarea.offsetWidth,
638 h: height
640 textarea.style.display = "none";
642 // create the editor framework
643 var htmlarea = document.createElement("div");
644 htmlarea.className = "htmlarea";
645 this._htmlArea = htmlarea;
647 // insert the editor before the textarea.
648 //Bug fix - unless the textarea is nested within its label, in which case insert editor before label.
649 if (textarea.parentNode.nodeName.toLowerCase()=='label') {
650 textarea.parentNode.parentNode.insertBefore(htmlarea,textarea.parentNode);
652 else {
653 textarea.parentNode.insertBefore(htmlarea, textarea);
656 if (textarea.form) {
657 // we have a form, on submit get the HTMLArea content and
658 // update original textarea.
659 var f = textarea.form;
660 if (typeof f.onsubmit == "function") {
661 var funcref = f.onsubmit;
662 if (typeof f.__msh_prevOnSubmit == "undefined") {
663 f.__msh_prevOnSubmit = [];
665 f.__msh_prevOnSubmit.push(funcref);
667 f.onsubmit = function() {
668 // Moodle hack. Bug fix #2736
669 var test = editor.getHTML();
670 test = test.replace(/<br \/>/gi, '');
671 test = test.replace(/\&nbsp\;/gi, '');
672 test = test.trim();
673 //alert(test + test.length);
674 if (test.length < 1) {
675 editor._textArea.value = test.trim();
676 } else {
677 editor._textArea.value = editor.getHTML();
679 // Moodle hack end.
680 var a = this.__msh_prevOnSubmit;
681 var ret = true;
682 // call previous submit methods if they were there.
683 if (typeof a != "undefined") {
684 for (var i = a.length; --i >= 0;) {
685 ret = a[i]() && ret;
688 return ret;
690 if (typeof f.onreset == "function") {
691 var funcref = f.onreset;
692 if (typeof f.__msh_prevOnReset == "undefined") {
693 f.__msh_prevOnReset = [];
695 f.__msh_prevOnReset.push(funcref);
697 f.onreset = function() {
698 editor.setHTML(editor._textArea.value);
699 editor.updateToolbar();
700 var a = this.__msh_prevOnReset;
701 // call previous reset methods if they were there.
702 if (typeof a != "undefined") {
703 for (var i = a.length; --i >= 0;) {
704 a[i]();
710 // add a handler for the "back/forward" case -- on body.unload we save
711 // the HTML content into the original textarea.
712 try {
713 window.onunload = function() {
714 editor._textArea.value = editor.getHTML();
716 } catch(e) {};
718 // creates & appends the toolbar
719 this._createToolbar();
721 // create the IFRAME
722 var iframe = document.createElement("iframe");
724 iframe.src = "about:blank";
726 iframe.className = "iframe";
728 htmlarea.appendChild(iframe);
730 var editor = this
731 editor._iframe = iframe;
732 var doc = editor._iframe.contentWindow.document;
733 editor._doc = doc;
735 // Generate iframe content
736 var html = ""
737 if (!editor.config.fullPage) {
738 html = "<html>\n";
739 html += "<head>\n";
740 html += '<meta http-equiv="content-type" content="text/html; charset=utf-8" />\n';
741 if (editor.config.baseURL)
742 html += '<base href="' + editor.config.baseURL + '" />';
743 html += '<style type="text/css">\n' + editor.config.pageStyle + "td { border: 1px dotted gray; } body { direction: <?php echo get_string('thisdirection')?>; } </style>\n"; // RTL support: direction added for RTL support
744 html += "</head>\n";
745 html += '<body>\n';
746 html += editor._textArea.value;
747 html = html.replace(/<nolink>/gi, '<span class="nolink">').
748 replace(/<\/nolink>/gi, '</span>');
749 html += "</body>\n";
750 html += "</html>";
751 } else {
752 html = editor._textArea.value;
753 if (html.match(HTMLArea.RE_doctype)) {
754 editor.setDoctype(RegExp.$1);
755 html = html.replace(HTMLArea.RE_doctype, "");
759 // Write content to iframe
760 doc.open();
761 doc.write(html);
762 doc.close();
764 // The magic: onClick the designMode is set to 'on'
765 // This one is for click on HTMLarea toolbar and else
766 if(HTMLArea.is_gecko) {
767 HTMLArea._addEvents(
768 this._htmlArea,
769 ["mousedown"],
770 function(event) {
771 if(editor.designModeIsOn != true)
773 editor.designModeIsOn = true;
774 try {
775 doc.designMode = "on";
776 } catch (e) {
777 alert(e)
783 // This one is for click in iframe
784 HTMLArea._addEvents(
785 editor._iframe.contentWindow,
786 ["mousedown"],
787 function(event) {
788 if(editor.designModeIsOn != true)
790 editor.designModeIsOn = true;
791 try {
792 doc.designMode = "on";
793 } catch (e) {
794 alert(e)
800 // creates & appends the status bar, if the case
801 this._createStatusBar();
803 // remove the default border as it keeps us from computing correctly
804 // the sizes. (somebody tell me why doesn't this work in IE)
806 if (!HTMLArea.is_ie) {
807 iframe.style.borderWidth = "1px";
810 // size the IFRAME according to user's prefs or initial textarea
811 var height = (this.config.height == "auto" ? (this._ta_size.h) : this.config.height);
812 height = parseInt(height);
813 var width = (this.config.width == "auto" ? (this._toolbar.offsetWidth) : this.config.width);
814 width = (width == 0 ? 598 : width);
815 //width = Math.max(parseInt(width), 598);
817 width = String(width);
818 if (width.match(/^\d+$/)) { // is this a pure int? if so, let it be in px, and remove 2px
819 height -= 2;
820 width -= 2;
821 width=width+"px";
824 iframe.style.width = width;
826 if (this.config.sizeIncludesToolbar) {
827 // substract toolbar height
828 height -= this._toolbar.offsetHeight;
829 height -= this._statusBar.offsetHeight;
831 if (height < 0) {
832 height = 0;
834 iframe.style.height = height + "px";
836 // the editor including the toolbar now have the same size as the
837 // original textarea.. which means that we need to reduce that a bit.
838 textarea.style.width = iframe.style.width;
839 textarea.style.height = iframe.style.height;
841 if (HTMLArea.is_ie) {
842 doc.body.contentEditable = true;
845 // intercept some events; for updating the toolbar & keyboard handlers
846 HTMLArea._addEvents
847 (doc, ["keydown", "keypress", "mousedown", "mouseup", "drag"],
848 function (event) {
849 return editor._editorEvent(HTMLArea.is_ie ? editor._iframe.contentWindow.event : event);
852 // check if any plugins have registered refresh handlers
853 for (var i in editor.plugins) {
854 var plugin = editor.plugins[i].instance;
855 if (typeof plugin.onGenerate == "function") {
856 plugin.onGenerate();
858 if (typeof plugin.onGenerateOnce == "function") {
859 plugin.onGenerateOnce();
860 plugin.onGenerateOnce = null;
864 // Moodle fix for bug Bug #2521 Too long statusbar line in IE
866 //setTimeout(function() {
867 // editor.updateToolbar();
868 //}, 250);
870 if (typeof editor.onGenerate == "function") {
871 editor.onGenerate();
876 // Switches editor mode; parameter can be "textmode" or "wysiwyg". If no
877 // parameter was passed this function toggles between modes.
878 HTMLArea.prototype.setMode = function(mode) {
879 if (typeof mode == "undefined") {
880 mode = ((this._editMode == "textmode") ? "wysiwyg" : "textmode");
882 switch (mode) {
883 case "textmode":
884 this._textArea.value = this.getHTML();
885 this._iframe.style.display = "none";
886 this._textArea.style.display = "block";
887 if (this.config.statusBar) {
888 while(this._statusBar.childNodes.length>0) {
889 this._statusBar.removeChild(this._statusBar.childNodes[0]);
892 this._statusBar.appendChild(document.createTextNode(HTMLArea.I18N.msg["TEXT_MODE"]));
894 break;
895 case "wysiwyg":
896 if (HTMLArea.is_gecko) {
897 // disable design mode before changing innerHTML
898 try {
899 this._doc.designMode = "off";
900 } catch(e) {};
902 if (!this.config.fullPage)
903 this._doc.body.innerHTML = this.getHTML();
904 else
905 this.setFullHTML(this.getHTML());
906 this._iframe.style.display = "block";
907 this._textArea.style.display = "none";
908 if (HTMLArea.is_gecko) {
909 // we need to refresh that info for Moz-1.3a
910 try {
911 this._doc.designMode = "on";
912 //this._doc.focus();
913 } catch(e) {};
915 if (this.config.statusBar) {
916 this._statusBar.innerHTML = '';
917 this._statusBar.appendChild(this._statusBarTree);
919 break;
920 default:
921 alert("Mode <" + mode + "> not defined!");
922 return false;
924 this._editMode = mode;
925 this.focusEditor();
928 HTMLArea.prototype.setFullHTML = function(html) {
929 var save_multiline = RegExp.multiline;
930 RegExp.multiline = true;
931 if (html.match(HTMLArea.RE_doctype)) {
932 this.setDoctype(RegExp.$1);
933 html = html.replace(HTMLArea.RE_doctype, "");
935 RegExp.multiline = save_multiline;
936 if (!HTMLArea.is_ie) {
937 if (html.match(HTMLArea.RE_head))
938 this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1;
939 if (html.match(HTMLArea.RE_body))
940 this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1;
941 } else {
942 var html_re = /<html>((.|\n)*?)<\/html>/i;
943 html = html.replace(html_re, "$1");
944 this._doc.open();
945 this._doc.write(html);
946 this._doc.close();
947 this._doc.body.contentEditable = true;
948 return true;
952 // Category: PLUGINS
954 HTMLArea.prototype.registerPlugin2 = function(plugin, args) {
955 if (typeof plugin == "string")
956 plugin = eval(plugin);
957 var obj = new plugin(this, args);
958 if (obj) {
959 var clone = {};
960 var info = plugin._pluginInfo;
961 for (var i in info)
962 clone[i] = info[i];
963 clone.instance = obj;
964 clone.args = args;
965 this.plugins[plugin._pluginInfo.name] = clone;
966 } else
967 alert("Can't register plugin " + plugin.toString() + ".");
970 // Create the specified plugin and register it with this HTMLArea
971 HTMLArea.prototype.registerPlugin = function() {
972 var plugin = arguments[0];
973 var args = [];
974 for (var i = 1; i < arguments.length; ++i)
975 args.push(arguments[i]);
976 this.registerPlugin2(plugin, args);
979 HTMLArea.loadPlugin = function(pluginName) {
980 var dir = _editor_url + "plugins/" + pluginName;
981 var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g,
982 function (str, l1, l2, l3) {
983 return l1 + "-" + l2.toLowerCase() + l3;
984 }).toLowerCase() + ".js";
985 var plugin_file = dir + "/" + plugin;
986 var plugin_lang = dir + "/lang/" + HTMLArea.I18N.lang + ".js";
987 HTMLArea._scripts.push(plugin_file, plugin_lang);
988 document.write("<script type='text/javascript' src='" + plugin_file + "'></script>");
989 document.write("<script type='text/javascript' src='" + plugin_lang + "'></script>");
992 HTMLArea.loadStyle = function(style, plugin) {
993 var url = _editor_url || '';
994 if (typeof plugin != "undefined") {
995 url += "plugins/" + plugin + "/";
997 url += style;
998 document.write("<style type='text/css'>@import url(" + url + ");</style>");
1000 HTMLArea.loadStyle("htmlarea.css");
1002 // Category: EDITOR UTILITIES
1004 // The following function is a slight variation of the word cleaner code posted
1005 // by Weeezl (user @ InteractiveTools forums).
1006 HTMLArea.prototype._wordClean = function() {
1007 this._unnestBlocks();
1009 var D = this.getInnerHTML();
1010 if (/[Mm]so/.test(D)) {
1012 // make one line
1013 D = D.replace(/\r\n/g, '\[br\]').
1014 replace(/\n/g, '').
1015 replace(/\r/g, '').
1016 replace(/\&nbsp\;/g,' ');
1018 // keep tags, strip attributes
1019 D = D.replace(/ class=[^\s|>]*/gi,'').
1020 //replace(/<p [^>]*TEXT-ALIGN: justify[^>]*>/gi,'<p align="justify">').
1021 replace(/ style=\"[^>]*\"/gi,'').
1022 replace(/ align=[^\s|>]*/gi,'');
1024 //clean up tags
1025 D = D.replace(/<b [^>]*>/gi,'<b>').
1026 replace(/<i [^>]*>/gi,'<i>').
1027 replace(/<li [^>]*>/gi,'<li>').
1028 replace(/<ul [^>]*>/gi,'<ul>');
1030 // replace outdated tags
1031 D = D.replace(/<b>/gi,'<strong>').
1032 replace(/<\/b>/gi,'</strong>');
1034 // mozilla doesn't like <em> tags
1035 D = D.replace(/<em>/gi,'<i>').
1036 replace(/<\/em>/gi,'</i>');
1038 // kill unwanted tags
1039 D = D.replace(/<\?xml:[^>]*>/g, ''). // Word xml
1040 replace(/<\/?st1:[^>]*>/g,''). // Word SmartTags
1041 replace(/<\/?[a-z]\:[^>]*>/g,''). // All other funny Word non-HTML stuff
1042 replace(/<\/?personname[^>]*>/gi,'').
1043 replace(/<\/?font[^>]*>/gi,''). // Disable if you want to keep font formatting
1044 replace(/<\/?span[^>]*>/gi,' ').
1045 replace(/<\/?div[^>]*>/gi,' ').
1046 replace(/<\/?pre[^>]*>/gi,' ').
1047 replace(/<(\/?)(h[1-6]+)[^>]*>/gi,'<$1$2>');
1049 // Lorenzo Nicola's addition
1050 // to get rid off silly word generated tags.
1051 D = D.replace(/<!--\[[^\]]*\]-->/gi,' ');
1053 //remove empty tags
1054 //D = D.replace(/<strong><\/strong>/gi,'').
1055 //replace(/<i><\/i>/gi,'').
1056 //replace(/<P[^>]*><\/P>/gi,'');
1057 D = D.replace(/<h[1-6]+>\s?<\/h[1-6]+>/gi, ''); // Remove empty headings
1059 // nuke double tags
1060 oldlen = D.length + 1;
1061 while(oldlen > D.length) {
1062 oldlen = D.length;
1063 // join us now and free the tags, we'll be free hackers, we'll be free... ;-)
1064 D = D.replace(/<([a-z][a-z]*)> *<\/\1>/gi,' ').
1065 replace(/<([a-z][a-z]*)> *<([a-z][^>]*)> *<\/\1>/gi,'<$2>');
1067 D = D.replace(/<([a-z][a-z]*)><\1>/gi,'<$1>').
1068 replace(/<\/([a-z][a-z]*)><\/\1>/gi,'<\/$1>');
1070 // nuke double spaces
1071 D = D.replace(/ */gi,' ');
1073 // Split into lines and remove
1074 // empty lines and add carriage returns back
1075 var splitter = /\[br\]/g;
1076 var emptyLine = /^\s+\s+$/g;
1077 var strHTML = '';
1078 var toLines = D.split(splitter);
1079 for (var i = 0; i < toLines.length; i++) {
1080 var line = toLines[i];
1081 if (line.length < 1) {
1082 continue;
1085 if (emptyLine.test(line)) {
1086 continue;
1089 line = line.replace(/^\s+\s+$/g, '');
1090 strHTML += line + '\n';
1092 D = strHTML;
1093 strHTML = '';
1095 this.setHTML(D);
1096 this.updateToolbar();
1100 HTMLArea.prototype._unnestBlockWalk = function(node, unnestingParent) {
1101 if (HTMLArea.RE_blocktag.test(node.nodeName)) {
1102 if (unnestingParent) {
1103 if (node.nextSibling) {
1104 var splitNode = this._doc.createElement(unnestingParent.nodeName.toLowerCase());
1105 while (node.nextSibling) {
1106 splitNode.appendChild(node.nextSibling);
1108 unnestingParent.parentNode.insertBefore(splitNode, unnestingParent.nextSibling);
1110 unnestingParent.parentNode.insertBefore(node, unnestingParent.nextSibling);
1111 return;
1113 else if (node.firstChild) {
1114 this._unnestBlockWalk(node.firstChild, node);
1116 } else {
1117 if (node.firstChild) {
1118 this._unnestBlockWalk(node.firstChild, null);
1121 if (node.nextSibling) {
1122 this._unnestBlockWalk(node.nextSibling, unnestingParent);
1126 HTMLArea.prototype._unnestBlocks = function() {
1127 this._unnestBlockWalk(this._doc.documentElement, null);
1130 HTMLArea.prototype.forceRedraw = function() {
1131 this._doc.body.style.visibility = "hidden";
1132 this._doc.body.style.visibility = "visible";
1133 // this._doc.body.innerHTML = this.getInnerHTML();
1136 // focuses the iframe window. returns a reference to the editor document.
1137 HTMLArea.prototype.focusEditor = function() {
1138 switch (this._editMode) {
1139 case "wysiwyg" : this._iframe.contentWindow.focus(); break;
1140 case "textmode": this._textArea.focus(); break;
1141 default : alert("ERROR: mode " + this._editMode + " is not defined");
1143 return this._doc;
1146 // takes a snapshot of the current text (for undo)
1147 HTMLArea.prototype._undoTakeSnapshot = function() {
1148 ++this._undoPos;
1149 if (this._undoPos >= this.config.undoSteps) {
1150 // remove the first element
1151 this._undoQueue.shift();
1152 --this._undoPos;
1154 // use the fasted method (getInnerHTML);
1155 var take = true;
1156 var txt = this.getInnerHTML();
1157 if (this._undoPos > 0)
1158 take = (this._undoQueue[this._undoPos - 1] != txt);
1159 if (take) {
1160 this._undoQueue[this._undoPos] = txt;
1161 } else {
1162 this._undoPos--;
1166 HTMLArea.prototype.undo = function() {
1167 if (this._undoPos > 0) {
1168 var txt = this._undoQueue[--this._undoPos];
1169 if (txt) this.setHTML(txt);
1170 else ++this._undoPos;
1174 HTMLArea.prototype.redo = function() {
1175 if (this._undoPos < this._undoQueue.length - 1) {
1176 var txt = this._undoQueue[++this._undoPos];
1177 if (txt) this.setHTML(txt);
1178 else --this._undoPos;
1182 // updates enabled/disable/active state of the toolbar elements
1183 HTMLArea.prototype.updateToolbar = function(noStatus) {
1184 var doc = this._doc;
1185 var text = (this._editMode == "textmode");
1186 var ancestors = null;
1187 if (!text) {
1188 ancestors = this.getAllAncestors();
1189 if (this.config.statusBar && !noStatus) {
1191 while(this._statusBarTree.childNodes.length>0) {
1192 this._statusBarTree.removeChild(this._statusBarTree.childNodes[0]);
1195 this._statusBarTree.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
1197 for (var i = ancestors.length; --i >= 0;) {
1198 var el = ancestors[i];
1199 if (!el) {
1200 // hell knows why we get here; this
1201 // could be a classic example of why
1202 // it's good to check for conditions
1203 // that are impossible to happen ;-)
1204 continue;
1206 var a = document.createElement("a");
1207 a.href = "#";
1208 a.el = el;
1209 a.editor = this;
1210 a.onclick = function() {
1211 this.blur();
1212 this.editor.selectNodeContents(this.el);
1213 this.editor.updateToolbar(true);
1214 return false;
1216 a.oncontextmenu = function() {
1217 // TODO: add context menu here
1218 this.blur();
1219 var info = "Inline style:\n\n";
1220 info += this.el.style.cssText.split(/;\s*/).join(";\n");
1221 alert(info);
1222 return false;
1224 var txt = el.tagName.toLowerCase();
1225 a.title = el.style.cssText;
1226 if (el.id) {
1227 txt += "#" + el.id;
1229 if (el.className) {
1230 txt += "." + el.className;
1232 a.appendChild(document.createTextNode(txt));
1233 this._statusBarTree.appendChild(a);
1234 if (i != 0) {
1235 this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb)));
1240 for (var i in this._toolbarObjects) {
1241 var btn = this._toolbarObjects[i];
1242 var cmd = i;
1243 var inContext = true;
1244 if (btn.context && !text) {
1245 inContext = false;
1246 var context = btn.context;
1247 var attrs = [];
1248 if (/(.*)\[(.*?)\]/.test(context)) {
1249 context = RegExp.$1;
1250 attrs = RegExp.$2.split(",");
1252 context = context.toLowerCase();
1253 var match = (context == "*");
1254 for (var k in ancestors) {
1255 if (!ancestors[k]) {
1256 // the impossible really happens.
1257 continue;
1259 if (match || (ancestors[k].tagName.toLowerCase() == context)) {
1260 inContext = true;
1261 for (var ka in attrs) {
1262 if (!eval("ancestors[k]." + attrs[ka])) {
1263 inContext = false;
1264 break;
1267 if (inContext) {
1268 break;
1273 btn.state("enabled", (!text || btn.text) && inContext);
1274 if (typeof cmd == "function") {
1275 continue;
1277 // look-it-up in the custom dropdown boxes
1278 var dropdown = this.config.customSelects[cmd];
1279 if ((!text || btn.text) && (typeof dropdown != "undefined")) {
1280 dropdown.refresh(this);
1281 continue;
1283 switch (cmd) {
1284 case "fontname":
1285 case "fontsize":
1286 case "formatblock":
1287 if (!text) try {
1288 var value = ("" + doc.queryCommandValue(cmd)).toLowerCase();
1289 if (!value) {
1290 // FIXME: what do we do here?
1291 break;
1293 var options = this.config[cmd];
1294 var k = 0;
1295 // btn.element.selectedIndex = 0;
1296 for (var j in options) {
1297 // FIXME: the following line is scary.
1298 if ((j.toLowerCase() == value) ||
1299 (options[j].substr(0, value.length).toLowerCase() == value)) {
1300 btn.element.selectedIndex = k;
1301 break;
1303 ++k;
1305 } catch(e) {};
1306 break;
1307 case "language":
1308 if (!text) try {
1309 var value;
1310 parentEl = this.getParentElement();
1311 if (parentEl.getAttribute('lang')) {
1312 // A language was previously defined for the block.
1313 if (parentEl.getAttribute('class') == 'multilang') {
1314 value = parentEl.getAttribute('lang')+'_ML';
1315 } else {
1316 value = parentEl.getAttribute('lang');
1318 } else {
1319 value = '';
1321 var options = this.config[cmd];
1322 var k = 0;
1323 for (var j in options) {
1324 // FIXME: the following line is scary.
1325 if ((j.toLowerCase() == value) ||
1326 (options[j].substr(0, value.length).toLowerCase() == value)) {
1327 btn.element.selectedIndex = k;
1328 break;
1330 ++k;
1332 } catch(e) {};
1333 break;
1334 case "textindicator":
1335 if (!text) {
1336 try {with (btn.element.style) {
1337 backgroundColor = HTMLArea._makeColor(
1338 doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor"));
1339 if (/transparent/i.test(backgroundColor)) {
1340 // Mozilla
1341 backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor"));
1343 color = HTMLArea._makeColor(doc.queryCommandValue("forecolor"));
1344 fontFamily = doc.queryCommandValue("fontname");
1345 fontWeight = doc.queryCommandState("bold") ? "bold" : "normal";
1346 fontStyle = doc.queryCommandState("italic") ? "italic" : "normal";
1347 }} catch (e) {
1348 // alert(e + "\n\n" + cmd);
1351 break;
1352 case "htmlmode": btn.state("active", text); break;
1353 case "lefttoright":
1354 case "righttoleft":
1355 var el = this.getParentElement();
1356 while (el && !HTMLArea.isBlockElement(el))
1357 el = el.parentNode;
1358 if (el)
1359 btn.state("active", (el.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr")));
1360 break;
1361 default:
1362 try {
1363 btn.state("active", (!text && doc.queryCommandState(cmd)));
1364 } catch (e) {}
1367 // take undo snapshots
1368 if (this._customUndo && !this._timerUndo) {
1369 this._undoTakeSnapshot();
1370 var editor = this;
1371 this._timerUndo = setTimeout(function() {
1372 editor._timerUndo = null;
1373 }, this.config.undoTimeout);
1375 // check if any plugins have registered refresh handlers
1376 for (var i in this.plugins) {
1377 var plugin = this.plugins[i].instance;
1378 if (typeof plugin.onUpdateToolbar == "function")
1379 plugin.onUpdateToolbar();
1383 /** Returns a node after which we can insert other nodes, in the current
1384 * selection. The selection is removed. It splits a text node, if needed.
1386 HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) {
1387 if (!HTMLArea.is_ie) {
1388 var sel = this._getSelection();
1389 var range = this._createRange(sel);
1390 // remove the current selection
1391 sel.removeAllRanges();
1392 range.deleteContents();
1393 var node = range.startContainer;
1394 var pos = range.startOffset;
1395 switch (node.nodeType) {
1396 case 3: // Node.TEXT_NODE
1397 // we have to split it at the caret position.
1398 if (toBeInserted.nodeType == 3) {
1399 // do optimized insertion
1400 node.insertData(pos, toBeInserted.data);
1401 range = this._createRange();
1402 range.setEnd(node, pos + toBeInserted.length);
1403 range.setStart(node, pos + toBeInserted.length);
1404 sel.addRange(range);
1405 } else {
1406 node = node.splitText(pos);
1407 var selnode = toBeInserted;
1408 if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
1409 selnode = selnode.firstChild;
1411 node.parentNode.insertBefore(toBeInserted, node);
1412 this.selectNodeContents(selnode);
1413 this.updateToolbar();
1415 break;
1416 case 1: // Node.ELEMENT_NODE
1417 var selnode = toBeInserted;
1418 if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
1419 selnode = selnode.firstChild;
1421 node.insertBefore(toBeInserted, node.childNodes[pos]);
1422 this.selectNodeContents(selnode);
1423 this.updateToolbar();
1424 break;
1426 } else {
1427 return null; // this function not yet used for IE <FIXME>
1431 // Returns the deepest node that contains both endpoints of the selection.
1432 HTMLArea.prototype.getParentElement = function() {
1433 var sel = this._getSelection();
1434 var range = this._createRange(sel);
1435 if (HTMLArea.is_ie) {
1436 switch (sel.type) {
1437 case "Text":
1438 case "None":
1439 return range.parentElement();
1440 case "Control":
1441 return range.item(0);
1442 default:
1443 return this._doc.body;
1445 } else try {
1446 var p = range.commonAncestorContainer;
1447 if (!range.collapsed && range.startContainer == range.endContainer &&
1448 range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes())
1449 p = range.startContainer.childNodes[range.startOffset];
1451 alert(range.startContainer + ":" + range.startOffset + "\n" +
1452 range.endContainer + ":" + range.endOffset);
1454 while (p.nodeType == 3) {
1455 p = p.parentNode;
1457 return p;
1458 } catch (e) {
1459 return null;
1463 // Returns an array with all the ancestor nodes of the selection.
1464 HTMLArea.prototype.getAllAncestors = function() {
1465 var p = this.getParentElement();
1466 var a = [];
1467 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1468 a.push(p);
1469 p = p.parentNode;
1471 a.push(this._doc.body);
1472 return a;
1475 // Selects the contents inside the given node
1476 HTMLArea.prototype.selectNodeContents = function(node, pos) {
1477 this.focusEditor();
1478 this.forceRedraw();
1479 var range;
1480 var collapsed = (typeof pos != "undefined");
1481 if (HTMLArea.is_ie) {
1482 range = this._doc.body.createTextRange();
1483 range.moveToElementText(node);
1484 (collapsed) && range.collapse(pos);
1485 range.select();
1486 } else {
1487 var sel = this._getSelection();
1488 range = this._doc.createRange();
1489 range.selectNodeContents(node);
1490 (collapsed) && range.collapse(pos);
1491 sel.removeAllRanges();
1492 sel.addRange(range);
1496 // Call this function to insert HTML code at the current position. It deletes
1497 // the selection, if any.
1498 HTMLArea.prototype.insertHTML = function(html) {
1499 var sel = this._getSelection();
1500 var range = this._createRange(sel);
1501 if (HTMLArea.is_ie) {
1502 range.pasteHTML(html);
1503 } else {
1504 // construct a new document fragment with the given HTML
1505 var fragment = this._doc.createDocumentFragment();
1506 var div = this._doc.createElement("div");
1507 div.innerHTML = html;
1508 while (div.firstChild) {
1509 // the following call also removes the node from div
1510 fragment.appendChild(div.firstChild);
1512 // this also removes the selection
1513 var node = this.insertNodeAtSelection(fragment);
1517 // Call this function to surround the existing HTML code in the selection with
1518 // your tags. FIXME: buggy! This function will be deprecated "soon".
1519 HTMLArea.prototype.surroundHTML = function(startTag, endTag) {
1520 var html = this.getSelectedHTML();
1521 // the following also deletes the selection
1522 this.insertHTML(startTag + html + endTag);
1525 /// Retrieve the selected block
1526 HTMLArea.prototype.getSelectedHTML = function() {
1527 var sel = this._getSelection();
1528 var range = this._createRange(sel);
1529 var existing = null;
1530 if (HTMLArea.is_ie) {
1531 existing = range.htmlText;
1532 } else {
1533 existing = HTMLArea.getHTML(range.cloneContents(), false, this);
1535 return existing;
1538 /// Return true if we have some selection
1539 HTMLArea.prototype.hasSelectedText = function() {
1540 // FIXME: come _on_ mishoo, you can do better than this ;-)
1541 return this.getSelectedHTML() != '';
1544 HTMLArea.prototype._createLink = function(link) {
1545 var editor = this;
1546 var allinks = editor._doc.getElementsByTagName('A');
1547 var anchors = new Array();
1548 for(var i = 0; i < allinks.length; i++) {
1549 var attrname = allinks[i].getAttribute('name');
1550 if((HTMLArea.is_ie ? attrname.length > 0 : attrname != null)) {
1551 anchors[i] = allinks[i].getAttribute('name');
1554 var outparam = null;
1555 if (typeof link == "undefined") {
1556 link = this.getParentElement();
1557 if (link && !/^a$/i.test(link.tagName)) {
1558 if(link.tagName.toLowerCase() != 'img') {
1559 link = null;
1560 var sel = this._getSelection();
1561 var rng = this._createRange(sel);
1562 var len = HTMLArea.is_ie ? rng.text.toString().length : sel.toString().length;
1563 if(len < 1) {
1564 alert("<?php print_string("alertnoselectedtext","editor");?>");
1565 return false;
1568 link = null;
1571 if (link) {
1572 outparam = {
1573 f_href : HTMLArea.is_ie ? editor.stripBaseURL(link.href) : link.getAttribute("href"),
1574 f_title : link.title,
1575 f_target : link.target,
1576 f_anchors: anchors
1578 } else {
1579 outparam = {
1580 f_anchors:anchors };
1582 this._popupDialog("link_std.php?id=<?php echo $id; ?>", function(param) {
1583 if (!param) {
1584 return false;
1586 var a = link;
1587 if (!a) {
1588 // Create a temporary unique link, insert it then find it and set the correct parameters
1589 var tmpLink = 'http://www.moodle.org/'+Math.random();
1590 var elm = editor._doc.execCommand("createlink",false,tmpLink);
1591 var links=editor._doc.getElementsByTagName("a");
1592 for(var i=0;i<links.length;i++){
1593 var link=links[i];
1594 if(link.href==tmpLink) {
1595 link.href=param.f_href.trim();
1596 if(param.f_target){
1597 link.target=param.f_target.trim();
1599 if(param.f_title){
1600 link.title=param.f_title.trim();
1602 break;
1605 } else {
1606 var href = param.f_href.trim();
1607 editor.selectNodeContents(a);
1608 if (href == "") {
1609 editor._doc.execCommand("unlink", false, null);
1610 editor.updateToolbar();
1611 return false;
1612 } else {
1613 a.href = href;
1616 if (!(a && /^a$/i.test(a.tagName))) {
1617 return false;
1619 a.target = param.f_target.trim();
1620 a.title = param.f_title.trim();
1621 editor.selectNodeContents(a);
1622 editor.updateToolbar();
1623 }, outparam);
1626 // Called when the user clicks on "InsertImage" button. If an image is already
1627 // there, it will just modify it's properties.
1628 HTMLArea.prototype._insertImage = function(image) {
1630 // Make sure that editor has focus
1631 this.focusEditor();
1632 var editor = this; // for nested functions
1633 var outparam = null;
1634 if (typeof image == "undefined") {
1635 image = this.getParentElement();
1636 if (image && !/^img$/i.test(image.tagName))
1637 image = null;
1639 if (image) outparam = {
1640 f_url : HTMLArea.is_ie ? editor.stripBaseURL(image.src) : image.getAttribute("src"),
1641 f_alt : image.alt,
1642 f_border : image.border,
1643 f_align : image.align,
1644 f_vert : image.vspace,
1645 f_horiz : image.hspace,
1646 f_width : image.width,
1647 f_height : image.height
1649 this._popupDialog("<?php
1650 if(!empty($id) and has_capability('moodle/course:managefiles', get_context_instance(CONTEXT_COURSE, $id))) {
1651 echo "insert_image.php?id=$id";
1652 } else {
1653 echo "insert_image_std.php?id=$id";
1654 }?>", function(param) {
1655 if (!param) { // user must have pressed Cancel
1656 return false;
1658 var img = image;
1659 if (!img) {
1660 var sel = editor._getSelection();
1661 var range = editor._createRange(sel);
1662 if (HTMLArea.is_ie) {
1663 editor._doc.execCommand("insertimage", false, param.f_url);
1665 if (HTMLArea.is_ie) {
1666 img = range.parentElement();
1667 // wonder if this works...
1668 if (img.tagName.toLowerCase() != "img") {
1669 img = img.previousSibling;
1671 } else {
1672 // MOODLE HACK: startContainer.perviousSibling
1673 // Doesn't work so we'll use createElement and
1674 // insertNodeAtSelection
1675 //img = range.startContainer.previousSibling;
1676 var img = editor._doc.createElement("img");
1677 img.setAttribute("src",""+ param.f_url +"");
1678 img.setAttribute("alt",""+ param.f_alt +"");
1679 editor.insertNodeAtSelection(img);
1681 } else {
1682 img.src = param.f_url;
1684 for (field in param) {
1685 var value = param[field];
1686 switch (field) {
1687 case "f_alt" : img.alt = value; img.title = value; break;
1688 case "f_border" : img.border = parseInt(value || "0"); break;
1689 case "f_align" : img.align = value; break;
1690 case "f_vert" : img.vspace = parseInt(value || "0"); break;
1691 case "f_horiz" : img.hspace = parseInt(value || "0"); break;
1692 case "f_width" :
1693 if(value != 0) {
1694 img.width = parseInt(value);
1695 } else {
1696 break;
1698 break;
1699 case "f_height" :
1700 if(value != 0) {
1701 img.height = parseInt(value);
1702 } else {
1703 break;
1705 break;
1708 }, outparam);
1711 // Called when the user clicks the Insert Table button
1712 HTMLArea.prototype._insertTable = function() {
1713 var sel = this._getSelection();
1714 var range = this._createRange(sel);
1715 var editor = this; // for nested functions
1716 this._popupDialog("insert_table.php?id=<?php echo $id; ?>", function(param) {
1717 if (!param) { // user must have pressed Cancel
1718 return false;
1720 var doc = editor._doc;
1721 // create the table element
1722 var table = doc.createElement("table");
1723 // assign the given arguments
1724 for (var field in param) {
1725 var value = param[field];
1726 if (!value) {
1727 continue;
1729 switch (field) {
1730 case "f_width" : table.width = value + param["f_unit"]; break;
1731 case "f_align" : table.align = value; break;
1732 case "f_border" : table.border = parseInt(value); break;
1733 case "f_spacing" : table.cellspacing = parseInt(value); break;
1734 case "f_padding" : table.cellpadding = parseInt(value); break;
1737 var tbody = doc.createElement("tbody");
1738 table.appendChild(tbody);
1739 for (var i = 0; i < param["f_rows"]; ++i) {
1740 var tr = doc.createElement("tr");
1741 tbody.appendChild(tr);
1742 for (var j = 0; j < param["f_cols"]; ++j) {
1743 var td = doc.createElement("td");
1744 /// Moodle hack
1745 if(param["f_unit"] == "px") {
1746 tdwidth = Math.round(table.width / param["f_cols"]);
1747 } else {
1748 tdwidth = Math.round(100 / param["f_cols"]);
1750 td.setAttribute("width",tdwidth + param["f_unit"]);
1751 td.setAttribute("valign","top");
1752 /// Moodle hack -ends
1753 tr.appendChild(td);
1754 // Mozilla likes to see something inside the cell.
1755 (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br"));
1758 if (HTMLArea.is_ie) {
1759 range.pasteHTML(table.outerHTML);
1760 } else {
1761 // insert the table
1762 editor.insertNodeAtSelection(table);
1764 return true;
1765 }, null);
1768 /// Moodle hack - insertSmile
1769 HTMLArea.prototype._insertSmile = function() {
1770 // Make sure that editor has focus
1771 this.focusEditor();
1772 var sel = this._getSelection();
1773 var range = this._createRange(sel);
1774 var editor = this; // for nested functions
1775 this._popupDialog("dlg_ins_smile.php?id=<?php echo $id; ?>", function(imgString) {
1776 if(!imgString) {
1777 return false;
1779 if (HTMLArea.is_ie) {
1780 range.pasteHTML(imgString);
1781 } else {
1782 editor.insertHTML(imgString);
1784 return true;
1785 }, null);
1788 HTMLArea.prototype._insertChar = function() {
1789 var sel = this._getSelection();
1790 var range = this._createRange(sel);
1791 var editor = this; // for nested functions
1792 this._popupDialog("dlg_ins_char.php?id=<?php echo $id; ?>", function(sChar) {
1793 if(!sChar) {
1794 return false;
1796 if (HTMLArea.is_ie) {
1797 range.pasteHTML(sChar);
1798 } else {
1799 // insert the table
1800 editor.insertHTML(sChar);
1802 return true;
1803 }, null);
1806 HTMLArea.prototype._removelink = function() {
1807 var editor = this;
1808 link = this.getParentElement();
1809 editor.selectNodeContents(link);
1811 this._doc.execCommand("unlink", false, null);
1812 this.focusEditor();
1815 HTMLArea.prototype._createanchor = function () {
1816 var editor = this;
1817 var sel = this._getSelection();
1818 var rng = this._createRange(sel);
1819 var len = HTMLArea.is_ie ? rng.text.toString().length : sel.toString().length;
1820 if(len < 1) {
1821 alert("<?php print_string("alertnoselectedtext","editor");?>");
1822 return false;
1824 this._popupDialog("createanchor.php?id=<?php echo $id; ?>", function(objAn) {
1825 if(!objAn) {
1826 return false;
1828 var str = '<a name="'+ objAn.anchor+'">';
1829 str += HTMLArea.is_ie ? rng.text : sel ;
1830 str += '</a>';
1831 editor.insertHTML(str);
1832 },null);
1835 HTMLArea.prototype._nolinktag = function () {
1837 var editor = this;
1838 var sel = this._getSelection();
1839 var rng = this._createRange(sel);
1840 var len = HTMLArea.is_ie ? rng.text.toString().length : sel.toString().length;
1842 if (len < 1) {
1843 alert("<?php print_string("alertnoselectedtext","editor");?>");
1844 return false;
1846 var str = '<span class="nolink">';
1847 str += HTMLArea.is_ie ? rng.text : sel;
1848 str += '</span>';
1849 editor.insertHTML(str);
1850 this.focusEditor();
1854 HTMLArea.prototype._searchReplace = function() {
1856 var editor = this;
1857 var selectedtxt = "";
1858 <?php
1859 $strreplaced = addslashes(get_string('itemsreplaced','editor'));
1860 $strnotfound = addslashes(get_string('searchnotfound','editor'));
1862 var strReplaced = '<?php echo $strreplaced ?>';
1863 var strNotfound = '<?php echo $strnotfound ?>';
1864 var ile;
1866 //in source mode mozilla show errors, try diffrent method
1867 if (editor._editMode == "wysiwyg") {
1868 selectedtxt = editor.getSelectedHTML();
1869 } else {
1870 if (HTMLArea.is_ie) {
1871 selectedtxt = document.selection.createRange().text;
1872 } else {
1873 selectedtxt = getMozSelection(editor._textArea);
1877 outparam = {
1878 f_search : selectedtxt
1881 //Call Search And Replace popup window
1882 editor._popupDialog( "searchandreplace.php?id=<?php echo $id; ?>", function( entity ) {
1883 if ( !entity ) {
1884 //user must have pressed Cancel
1885 return false;
1887 var text = editor.getHTML();
1888 var search = entity[0];
1889 var replace = entity[1];
1890 var delim = entity[2];
1891 var regularx = entity[3];
1892 var closesar = entity[4];
1893 ile = 0;
1894 if (search.length < 1) {
1895 alert ("Enter a search word! \n search for: " + entity[0]);
1896 } else {
1897 if (regularx) {
1898 var regX = new RegExp (search, delim) ;
1899 var text = text.replace ( regX,
1900 function (str, n) {
1901 // Increment our counter variable.
1902 ile++ ;
1903 //return replace ;
1904 return str.replace( regX, replace) ;
1908 } else {
1909 while (text.indexOf(search)>-1) {
1910 pos = text.indexOf(search);
1911 text = "" + (text.substring(0, pos) + replace + text.substring((pos + search.length), text.length));
1912 ile++;
1916 editor.setHTML(text);
1917 editor.forceRedraw();
1918 if (ile > 0) {
1919 alert(ile + ' ' + strReplaced);
1920 } else {
1921 alert (strNotfound + "\n");
1924 }, outparam);
1926 function getMozSelection(txtarea) {
1927 var selLength = txtarea.textLength;
1928 var selStart = txtarea.selectionStart;
1929 var selEnd = txtarea.selectionEnd;
1930 if (selEnd==1 || selEnd==2) selEnd=selLength;
1931 return (txtarea.value).substring(selStart, selEnd);
1935 /// Moodle hack's ends
1937 // Category: EVENT HANDLERS
1939 // el is reference to the SELECT object
1940 // txt is the name of the select field, as in config.toolbar
1941 HTMLArea.prototype._comboSelected = function(el, txt) {
1942 this.focusEditor();
1943 var value = el.options[el.selectedIndex].value;
1944 switch (txt) {
1945 case "fontname":
1946 case "fontsize": this.execCommand(txt, false, value); break;
1947 case "language":
1948 this.setLang(value);
1949 break;
1950 case "formatblock":
1951 (HTMLArea.is_ie) && (value = "<" + value + ">");
1952 this.execCommand(txt, false, value);
1953 break;
1954 default:
1955 // try to look it up in the registered dropdowns
1956 var dropdown = this.config.customSelects[txt];
1957 if (typeof dropdown != "undefined") {
1958 dropdown.action(this);
1959 } else {
1960 alert("FIXME: combo box " + txt + " not implemented");
1967 * Used to set the language for the selected content.
1968 * We use the <span lang="en" class="multilang">content</span> format for
1969 * content that should be marked for multilang filter use, and
1970 * <span lang="en">content</span> for normal content for which we want to
1971 * set the language (for screen reader usage, for example).
1973 HTMLArea.prototype.setLang = function(lang) {
1975 if (lang == 'multi') {
1976 // This is just the separator in the dropdown. Does nothing.
1977 return;
1980 var editor = this;
1981 var selectedHTML = editor.getSelectedHTML();
1982 var multiLang = false;
1984 var re = new RegExp('_ML', 'g');
1985 if (lang.match(re)) {
1986 multiLang = true;
1987 lang = lang.replace(re, '');
1990 // Remove all lang attributes from span tags in selected html.
1991 selectedHTML = selectedHTML.replace(/(<span[^>]*)lang="[^"]*"([^>]*>)/, "$1$2");
1992 selectedHTML = selectedHTML.replace(/(<span[^>]*)class="multilang"([^>]*>)/, "$1$2");
1994 // If a span tag is now empty, delete it.
1995 selectedHTML = selectedHTML.replace(/<span\s*>(.*?)<\/span>/, "$1");
1998 var parentEl = this.getParentElement();
1999 var insertNewSpan = false;
2001 if (parentEl.nodeName == 'SPAN' && parentEl.getAttribute('lang')) {
2002 // A language was previously defined for the current block.
2003 // Check whether the selected text makes up the whole of the block
2004 // contents.
2005 var re = new RegExp(parentEl.innerHTML);
2007 if (selectedHTML.match(re)) {
2008 // The selected text makes up the whole of the span block.
2009 if (lang != '') {
2010 parentEl.setAttribute('lang', lang);
2011 if (multiLang) {
2012 parentEl.setAttribute('class', 'multilang');
2014 } else {
2015 parentEl.removeAttribute('lang');
2017 var classAttr = parentEl.getAttribute('class');
2018 if (classAttr) {
2019 classAttr = classAttr.replace(/multilang/, '').trim();
2021 if (classAttr == '') {
2022 parentEl.removeAttribute('class');
2024 if (parentEl.attributes.length == 0) {
2025 // The span is no longer needed.
2026 for (i=0; i<parentEl.childNodes.length; i++) {
2027 parentEl.parentNode.insertBefore(parentEl.childNodes[i], parentEl);
2029 parentEl.parentNode.removeChild(parentEl);
2032 } else {
2033 insertNewSpan = true;
2035 } else {
2036 insertNewSpan = true;
2039 if (insertNewSpan && lang != '') {
2040 var str = '<span lang="'+lang.trim()+'"';
2041 str += ' class="multilang"';
2042 str += '>';
2043 str += selectedHTML;
2044 str += '</span>';
2045 editor.insertHTML(str);
2050 // the execCommand function (intercepts some commands and replaces them with
2051 // our own implementation)
2052 HTMLArea.prototype.execCommand = function(cmdID, UI, param) {
2053 var editor = this; // for nested functions
2054 this.focusEditor();
2055 cmdID = cmdID.toLowerCase();
2056 switch (cmdID) {
2057 case "htmlmode" : this.setMode(); break;
2058 case "hilitecolor":
2059 (HTMLArea.is_ie) && (cmdID = "backcolor");
2060 case "forecolor":
2061 this._popupDialog("select_color.php?id=<?php echo $id; ?>", function(color) {
2062 if (color) { // selection not canceled
2063 editor._doc.execCommand(cmdID, false, "#" + color);
2065 }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID)));
2066 break;
2067 case "createanchor": this._createanchor(); break;
2068 case "createlink":
2069 this._createLink();
2070 break;
2071 case "unlink": this._removelink(); break;
2072 case "nolink": this._nolinktag(); break;
2073 case "popupeditor":
2074 // this object will be passed to the newly opened window
2075 HTMLArea._object = this;
2076 if (HTMLArea.is_ie) {
2078 window.open(this.popupURL("fullscreen.php?id=<?php echo $id;?>"), "ha_fullscreen",
2079 "toolbar=no,location=no,directories=no,status=no,menubar=no," +
2080 "scrollbars=no,resizable=yes,width=800,height=600");
2082 } else {
2083 window.open(this.popupURL("fullscreen.php?id=<?php echo $id;?>"), "ha_fullscreen",
2084 "toolbar=no,menubar=no,personalbar=no,width=800,height=600," +
2085 "scrollbars=no,resizable=yes");
2087 break;
2088 case "undo":
2089 case "redo":
2090 if (this._customUndo)
2091 this[cmdID]();
2092 else
2093 this._doc.execCommand(cmdID, UI, param);
2094 break;
2095 case "inserttable": this._insertTable(); break;
2096 case "insertimage": this._insertImage(); break;
2097 case "insertsmile": this._insertSmile(); break;
2098 case "insertchar": this._insertChar(); break;
2099 case "searchandreplace": this._searchReplace(); break;
2100 case "about" : this._popupDialog("about.html", null, this); break;
2101 case "showhelp" : window.open(_editor_url + "reference.html", "ha_help"); break;
2103 case "killword": this._wordClean(); break;
2105 case "cut":
2106 case "copy":
2107 case "paste":
2108 try {
2109 // Paste first then clean
2110 this._doc.execCommand(cmdID, UI, param);
2111 if (this.config.killWordOnPaste) {
2112 this._wordClean();
2114 } catch (e) {
2115 if (HTMLArea.is_gecko) {
2116 if (confirm("<?php
2117 $strmoz = get_string('cutpastemozilla','editor');
2118 $strmoz = preg_replace("/[\n|\r]+/", "", $strmoz);
2119 $strmoz = str_replace('<br />', '\\n', $strmoz);
2121 echo addslashes($strmoz);
2123 ?>"))
2124 window.open("http://moodle.org/mozillahelp");
2127 break;
2128 case "lefttoright":
2129 case "righttoleft":
2130 var dir = (cmdID == "righttoleft") ? "rtl" : "ltr";
2131 var el = this.getParentElement();
2132 while (el && !HTMLArea.isBlockElement(el))
2133 el = el.parentNode;
2134 if (el) {
2135 if (el.style.direction == dir)
2136 el.style.direction = "";
2137 else
2138 el.style.direction = dir;
2140 break;
2141 default: this._doc.execCommand(cmdID, UI, param);
2143 this.updateToolbar();
2144 return false;
2149 * A generic event handler for things that happen in the IFRAME's document.
2150 * This function also handles key bindings.
2152 HTMLArea.prototype._editorEvent = function(ev) {
2154 var editor = this;
2155 var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress");
2157 if (keyEvent) {
2159 for (var i in editor.plugins) {
2160 var plugin = editor.plugins[i].instance;
2161 if (typeof plugin.onKeyPress == "function") plugin.onKeyPress(ev);
2164 var sel = null;
2165 var range = null;
2166 var key = String.fromCharCode(HTMLArea.is_ie ? ev.keyCode : ev.charCode).toLowerCase();
2167 var cmd = null;
2168 var value = null;
2170 if (ev.ctrlKey && !ev.altKey) {
2172 * Ctrl modifier only.
2173 * We use these for shortcuts that change existing content,
2174 * e.g. make text bold.
2176 switch (key) {
2178 case 'a':
2179 // Select all.
2180 if (!HTMLArea.is_ie) {
2181 // KEY select all
2182 sel = this._getSelection();
2183 sel.removeAllRanges();
2184 range = this._createRange();
2185 range.selectNodeContents(this._doc.body);
2186 sel.addRange(range);
2187 HTMLArea._stopEvent(ev);
2189 break;
2191 // For the dropdowns, we assign focus to them so that they are
2192 // keyboard accessible.
2193 case 'o':
2194 editor.dropdowns['fontname'].focus();
2195 break;
2196 case 'p':
2197 editor.dropdowns['fontsize'].focus();
2198 break;
2199 case 'h':
2200 editor.dropdowns['formatblock'].focus();
2201 break;
2202 case '=':
2203 editor.dropdowns['language'].focus();
2204 break;
2206 case 'b': cmd = "bold"; break;
2207 case 'i': cmd = "italic"; break;
2208 case 'u': cmd = "underline"; break;
2209 case 's': cmd = "strikethrough"; break;
2210 case ',': cmd = "subscript"; break;
2211 case '.': cmd = "superscript"; break;
2213 case 'v':
2214 if (! HTMLArea.is_gecko ) {
2215 cmd = "paste";
2217 break;
2219 case '0': cmd = "killword"; break;
2220 case 'z': cmd = "undo"; break;
2221 case 'y': cmd = "redo"; break;
2222 case 'l': cmd = "justifyleft"; break;
2223 case 'e': cmd = "justifycenter"; break;
2224 case 'r': cmd = "justifyright"; break;
2225 case 'j': cmd = "justifyfull"; break;
2226 case '/': cmd = "lefttoright"; break;
2227 case '|': cmd = "righttoleft"; break;
2228 case ';': cmd = "outdent"; break;
2229 case "'": cmd = "indent"; break;
2230 case 'g': cmd = "forecolor"; break;
2231 case 'k': cmd = "hilitecolor"; break;
2232 case 'f': cmd = "searchandreplace"; break;
2233 case '`': cmd = "htmlmode"; break; // FIXME: can't toggle from source code to wysiwyg
2235 case 'm':
2236 // Toggle fullscreen on or off.
2237 if (this.config.btnList['popupeditor'][0] == 'Enlarge Editor') {
2238 cmd = 'popupeditor';
2239 } else {
2240 window.close();
2242 break;
2244 // Headings.
2245 case '1':
2246 case '2':
2247 case '3':
2248 case '4':
2249 case '5':
2250 case '6':
2251 cmd = "formatblock";
2252 value = "h" + key;
2253 if (HTMLArea.is_ie) {
2254 value = "<" + value + ">";
2256 break;
2258 } // End switch (key)
2261 } else if (ev.ctrlKey && ev.altKey) {
2263 * Ctrl + Alt modifiers.
2264 * We use these for shortcuts that insert stuff, e.g. images.
2266 switch (key) {
2267 case 'o': cmd = "insertorderedlist"; break;
2268 case 'u': cmd = "insertunorderedlist"; break;
2269 case 'r': cmd = "inserthorizontalrule"; break;
2270 case 'a': cmd = "createanchor"; break;
2271 case 'l': cmd = "createlink"; break;
2272 case 'd': cmd = "unlink"; break;
2273 case 'n': cmd = "nolink"; break;
2274 case 'i': cmd = 'insertimage'; break;
2275 case 't': cmd = 'inserttable'; break;
2276 case 's': cmd = 'insertsmile'; break;
2277 case 'c': cmd = 'insertchar'; break;
2281 if (cmd) {
2282 // execute simple command
2283 this.execCommand(cmd, false, value);
2284 HTMLArea._stopEvent(ev);
2286 } // End if (keyEvent)
2289 else if (keyEvent) {
2290 // other keys here
2291 switch (ev.keyCode) {
2292 case 13: // KEY enter
2293 // if (HTMLArea.is_ie) {
2294 this.insertHTML("<br />");
2295 HTMLArea._stopEvent(ev);
2296 // }
2297 break;
2302 // Update the toolbar state after some time.
2303 if (editor._timerToolbar) {
2304 clearTimeout(editor._timerToolbar);
2306 editor._timerToolbar = setTimeout(function() {
2307 editor.updateToolbar();
2308 editor._timerToolbar = null;
2309 }, 50);
2313 // retrieve the HTML
2314 HTMLArea.prototype.getHTML = function() {
2315 switch (this._editMode) {
2316 case "wysiwyg" :
2317 if (!this.config.fullPage) {
2318 return HTMLArea.getHTML(this._doc.body, false, this);
2319 } else
2320 return this.doctype + "\n" + HTMLArea.getHTML(this._doc.documentElement, true, this);
2321 case "textmode" : return this._textArea.value;
2322 default : alert("Mode <" + mode + "> not defined!");
2324 return false;
2327 // retrieve the HTML (fastest version, but uses innerHTML)
2328 HTMLArea.prototype.getInnerHTML = function() {
2329 switch (this._editMode) {
2330 case "wysiwyg" :
2331 if (!this.config.fullPage)
2332 return this._doc.body.innerHTML;
2333 else
2334 return this.doctype + "\n" + this._doc.documentElement.innerHTML;
2335 case "textmode" : return this._textArea.value;
2336 default : alert("Mode <" + mode + "> not defined!");
2338 return false;
2341 // completely change the HTML inside
2342 HTMLArea.prototype.setHTML = function(html) {
2343 switch (this._editMode) {
2344 case "wysiwyg" :
2345 if (!this.config.fullPage)
2346 this._doc.body.innerHTML = html;
2347 else
2348 // this._doc.documentElement.innerHTML = html;
2349 this._doc.body.innerHTML = html;
2350 break;
2351 case "textmode" : this._textArea.value = html; break;
2352 default : alert("Mode <" + mode + "> not defined!");
2354 return false;
2357 // sets the given doctype (useful when config.fullPage is true)
2358 HTMLArea.prototype.setDoctype = function(doctype) {
2359 this.doctype = doctype;
2362 /***************************************************
2363 * Category: UTILITY FUNCTIONS
2364 ***************************************************/
2366 // browser identification
2368 HTMLArea.agt = navigator.userAgent.toLowerCase();
2369 HTMLArea.is_ie = ((HTMLArea.agt.indexOf("msie") != -1) && (HTMLArea.agt.indexOf("opera") == -1));
2370 HTMLArea.is_opera = (HTMLArea.agt.indexOf("opera") != -1);
2371 HTMLArea.is_mac = (HTMLArea.agt.indexOf("mac") != -1);
2372 HTMLArea.is_mac_ie = (HTMLArea.is_ie && HTMLArea.is_mac);
2373 HTMLArea.is_win_ie = (HTMLArea.is_ie && !HTMLArea.is_mac);
2374 HTMLArea.is_gecko = (navigator.product == "Gecko");
2375 HTMLArea.is_safari = (HTMLArea.agt.indexOf("safari") != -1);
2377 // variable used to pass the object to the popup editor window.
2378 HTMLArea._object = null;
2380 // function that returns a clone of the given object
2381 HTMLArea.cloneObject = function(obj) {
2382 var newObj = new Object;
2384 // check for array objects
2385 if (obj.constructor.toString().indexOf("function Array(") >= 0) {
2386 newObj = obj.constructor();
2389 // check for function objects (as usual, IE is phucked up)
2390 if (obj.constructor.toString().indexOf("function Function(") >= 0) {
2391 newObj = obj; // just copy reference to it
2392 } else for (var n in obj) {
2393 var node = obj[n];
2394 if (typeof node == 'object') { newObj[n] = HTMLArea.cloneObject(node); }
2395 else { newObj[n] = node; }
2398 return newObj;
2401 // FIXME!!! this should return false for IE < 5.5
2402 HTMLArea.checkSupportedBrowser = function() {
2403 if (HTMLArea.is_gecko) {
2404 if (navigator.productSub < 20021201) {
2405 alert("You need at least Mozilla-1.3 Alpha.\n" +
2406 "Sorry, your Gecko is not supported.");
2407 return false;
2409 if (navigator.productSub < 20030210) {
2410 alert("Mozilla < 1.3 Beta is not supported!\n" +
2411 "I'll try, though, but it might not work.");
2414 if(HTMLArea.is_safari) {
2415 return false;
2417 return HTMLArea.is_gecko || HTMLArea.is_ie;
2420 // selection & ranges
2422 // returns the current selection object
2423 HTMLArea.prototype._getSelection = function() {
2424 if (HTMLArea.is_ie) {
2425 return this._doc.selection;
2426 } else {
2427 return this._iframe.contentWindow.getSelection();
2431 // returns a range for the current selection
2432 HTMLArea.prototype._createRange = function(sel) {
2433 if (HTMLArea.is_ie) {
2434 return sel.createRange();
2435 } else {
2436 // Commented out because we need the dropdowns to be able to keep
2437 // focus for keyboard accessibility. Comment by Vy-Shane Sin Fat.
2438 //this.focusEditor();
2439 if (typeof sel != "undefined") {
2440 try {
2441 return sel.getRangeAt(0);
2442 } catch(e) {
2443 return this._doc.createRange();
2445 } else {
2446 return this._doc.createRange();
2451 // event handling
2453 HTMLArea._addEvent = function(el, evname, func) {
2454 if (HTMLArea.is_ie) {
2455 el.attachEvent("on" + evname, func);
2456 } else {
2457 el.addEventListener(evname, func, true);
2461 HTMLArea._addEvents = function(el, evs, func) {
2462 for (var i in evs) {
2463 HTMLArea._addEvent(el, evs[i], func);
2467 HTMLArea._removeEvent = function(el, evname, func) {
2468 if (HTMLArea.is_ie) {
2469 el.detachEvent("on" + evname, func);
2470 } else {
2471 el.removeEventListener(evname, func, true);
2475 HTMLArea._removeEvents = function(el, evs, func) {
2476 for (var i in evs) {
2477 HTMLArea._removeEvent(el, evs[i], func);
2481 HTMLArea._stopEvent = function(ev) {
2482 if (HTMLArea.is_ie) {
2483 ev.cancelBubble = true;
2484 ev.returnValue = false;
2485 } else {
2486 ev.preventDefault();
2487 ev.stopPropagation();
2491 HTMLArea._removeClass = function(el, className) {
2492 if (!(el && el.className)) {
2493 return;
2495 var cls = el.className.split(" ");
2496 var ar = new Array();
2497 for (var i = cls.length; i > 0;) {
2498 if (cls[--i] != className) {
2499 ar[ar.length] = cls[i];
2502 el.className = ar.join(" ");
2505 HTMLArea._addClass = function(el, className) {
2506 // remove the class first, if already there
2507 HTMLArea._removeClass(el, className);
2508 el.className += " " + className;
2511 HTMLArea._hasClass = function(el, className) {
2512 if (!(el && el.className)) {
2513 return false;
2515 var cls = el.className.split(" ");
2516 for (var i = cls.length; i > 0;) {
2517 if (cls[--i] == className) {
2518 return true;
2521 return false;
2524 HTMLArea.isBlockElement = function(el) {
2526 var blockTags = " body form textarea fieldset ul ol dl li div " +
2527 "p h1 h2 h3 h4 h5 h6 quote pre table thead " +
2528 "tbody tfoot tr td iframe address ";
2529 try {
2530 return (blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
2531 } catch (e) {}
2535 HTMLArea.needsClosingTag = function(el) {
2536 var closingTags = " head script style div span tr td tbody table em strong font a title iframe object applet ";
2537 return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
2540 // performs HTML encoding of some given string
2541 HTMLArea.htmlEncode = function(str) {
2542 // we don't need regexp for that, but.. so be it for now.
2543 str = str.replace(/&/ig, "&amp;");
2544 str = str.replace(/</ig, "&lt;");
2545 str = str.replace(/>/ig, "&gt;");
2546 str = str.replace(/\x22/ig, "&quot;");
2547 // \x22 means '"' -- we use hex reprezentation so that we don't disturb
2548 // JS compressors (well, at least mine fails.. ;)
2549 return str;
2552 HTMLArea.isStandardTag = function (el) {
2553 return HTMLArea.RE_msietag.test(el.tagName);
2555 HTMLArea.isSingleTag = function (el) {
2556 var re = /^(br|hr|img|input|link|meta|param|embed|area)$/i;
2557 return re.test(el.tagName.toLowerCase());
2559 // Retrieves the HTML code from the given node. This is a replacement for
2560 // getting innerHTML, using standard DOM calls.
2561 HTMLArea.getHTML = function(root, outputRoot, editor) {
2562 var html = "";
2563 switch (root.nodeType) {
2564 case 1: // Node.ELEMENT_NODE
2565 case 11: // Node.DOCUMENT_FRAGMENT_NODE
2566 var closed;
2567 var i;
2568 var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : '';
2569 if (HTMLArea.RE_junktag.test(root_tag)) {
2570 return '';
2572 if (HTMLArea.is_ie && root_tag == "head") {
2573 if (outputRoot)
2574 html += "<head>";
2575 // lowercasize
2576 var save_multiline = RegExp.multiline;
2577 RegExp.multiline = true;
2578 var txt = root.innerHTML.replace(HTMLArea.RE_tagName, function(str, p1, p2) {
2579 return p1 + p2.toLowerCase();
2581 RegExp.multiline = save_multiline;
2582 html += txt;
2583 if (outputRoot)
2584 html += "</head>";
2585 break;
2586 } else if (outputRoot) {
2587 closed = (!(root.hasChildNodes() || !HTMLArea.isSingleTag(root)));
2588 html = "<" + root.tagName.toLowerCase();
2589 var attrs = root.attributes;
2590 for (i = 0; i < attrs.length; ++i) {
2591 var a = attrs.item(i);
2592 if (!a.specified) {
2593 continue;
2595 var name = a.nodeName.toLowerCase();
2596 if (/_moz|contenteditable|_msh/.test(name)) {
2597 // avoid certain attributes
2598 continue;
2600 var value;
2601 if (name != "style") {
2603 // Using Gecko the values of href and src are converted to absolute links
2604 // unless we get them using nodeValue()
2605 if (typeof root[a.nodeName] != "undefined" && name != "href" && name != "src") {
2606 value = root[a.nodeName];
2607 } else {
2608 // This seems to be working, but if it does cause
2609 // problems later on return the old value...
2610 if (name.toLowerCase() == "href" && name.toLowerCase() == "src") {
2611 value = root[a.nodeName];
2612 } else {
2613 value = a.nodeValue;
2615 if (HTMLArea.is_ie && (name == "href" || name == "src")) {
2616 value = editor.stripBaseURL(value);
2619 } else { // IE fails to put style in attributes list
2620 // FIXME: cssText reported by IE is UPPERCASE
2621 value = root.style.cssText.toLowerCase();
2623 if (/(_moz|^$)/.test(value)) {
2624 // Mozilla reports some special tags
2625 // here; we don't need them.
2626 continue;
2628 html += " " + name + '="' + value + '"';
2630 html += closed ? " />" : ">";
2632 for (i = root.firstChild; i; i = i.nextSibling) {
2633 html += HTMLArea.getHTML(i, true, editor);
2635 if (outputRoot && !closed) {
2636 if ( HTMLArea.is_ie && !HTMLArea.isStandardTag(root) ) {
2637 html += '';
2638 } else {
2639 html += "</" + root.tagName.toLowerCase() + ">";
2642 break;
2643 case 3: // Node.TEXT_NODE
2644 // If a text node is alone in an element and all spaces, replace it with an non breaking one
2645 // This partially undoes the damage done by moz, which translates '&nbsp;'s into spaces in the data element
2646 if ( !root.previousSibling && !root.nextSibling && root.data.match(/^\s*$/i) && root.data.length > 1 ) html = '&nbsp;';
2647 else html = HTMLArea.htmlEncode(root.data);
2648 break;
2649 case 8: // Node.COMMENT_NODE
2650 html = "<!--" + root.data + "-->";
2651 break; // skip comments, for now.
2654 return HTMLArea.indent(html);
2657 HTMLArea.prototype.stripBaseURL = function(string) {
2658 var baseurl = this.config.baseURL;
2660 // IE adds the path to an anchor, converting #anchor
2661 // to path/#anchor which of course needs to be fixed
2662 var index = string.indexOf("/#")+1;
2663 if ((index > 0) && (string.indexOf(baseurl) > -1)) {
2664 return string.substr(index);
2666 return string; // Moodle doesn't use the code below because
2667 // Moodle likes to keep absolute links
2669 // strip to last directory in case baseurl points to a file
2670 baseurl = baseurl.replace(/[^\/]+$/, '');
2671 var basere = new RegExp(baseurl);
2672 string = string.replace(basere, "");
2674 // strip host-part of URL which is added by MSIE to links relative to server root
2675 baseurl = baseurl.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1');
2676 basere = new RegExp(baseurl);
2677 return string.replace(basere, "");
2680 String.prototype.trim = function() {
2681 a = this.replace(/^\s+/, '');
2682 return a.replace(/\s+$/, '');
2685 // creates a rgb-style color from a number
2686 HTMLArea._makeColor = function(v) {
2687 if (typeof v != "number") {
2688 // already in rgb (hopefully); IE doesn't get here.
2689 return v;
2691 // IE sends number; convert to rgb.
2692 var r = v & 0xFF;
2693 var g = (v >> 8) & 0xFF;
2694 var b = (v >> 16) & 0xFF;
2695 return "rgb(" + r + "," + g + "," + b + ")";
2698 // returns hexadecimal color representation from a number or a rgb-style color.
2699 HTMLArea._colorToRgb = function(v) {
2700 if (!v)
2701 return '';
2703 // returns the hex representation of one byte (2 digits)
2704 function hex(d) {
2705 return (d < 16) ? ("0" + d.toString(16)) : d.toString(16);
2708 if (typeof v == "number") {
2709 // we're talking to IE here
2710 var r = v & 0xFF;
2711 var g = (v >> 8) & 0xFF;
2712 var b = (v >> 16) & 0xFF;
2713 return "#" + hex(r) + hex(g) + hex(b);
2716 if (v.substr(0, 3) == "rgb") {
2717 // in rgb(...) form -- Mozilla
2718 var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/;
2719 if (v.match(re)) {
2720 var r = parseInt(RegExp.$1);
2721 var g = parseInt(RegExp.$2);
2722 var b = parseInt(RegExp.$3);
2723 return "#" + hex(r) + hex(g) + hex(b);
2725 // doesn't match RE?! maybe uses percentages or float numbers
2726 // -- FIXME: not yet implemented.
2727 return null;
2730 if (v.substr(0, 1) == "#") {
2731 // already hex rgb (hopefully :D )
2732 return v;
2735 // if everything else fails ;)
2736 return null;
2739 HTMLArea.prototype._popupDialog = function(url, action, init) {
2740 Dialog(this.popupURL(url), action, init);
2743 // paths
2745 HTMLArea.prototype.imgURL = function(file, plugin) {
2746 if (typeof plugin == "undefined")
2747 return _editor_url + file;
2748 else
2749 return _editor_url + "plugins/" + plugin + "/img/" + file;
2752 HTMLArea.prototype.popupURL = function(file) {
2753 var url = "";
2754 if (file.match(/^plugin:\/\/(.*?)\/(.*)/)) {
2755 var plugin = RegExp.$1;
2756 var popup = RegExp.$2;
2757 if (!/\.html$/.test(popup))
2758 popup += ".html";
2759 url = _editor_url + "plugins/" + plugin + "/popups/" + popup;
2760 } else
2761 url = _editor_url + this.config.popupURL + file;
2762 return url;
2766 * FIX: Internet Explorer returns an item having the _name_ equal to the given
2767 * id, even if it's not having any id. This way it can return a different form
2768 * field even if it's not a textarea. This workarounds the problem by
2769 * specifically looking to search only elements having a certain tag name.
2771 HTMLArea.getElementById = function(tag, id) {
2772 var el, i, objs = document.getElementsByTagName(tag);
2773 for (i = objs.length; --i >= 0 && (el = objs[i]);)
2774 if (el.id == id)
2775 return el;
2776 return null;
2778 // Modified version of GetHtml plugin's indent.
2779 HTMLArea.indent = function(s, sindentChar) {
2780 var c = [
2781 /*0*/ new RegExp().compile(/<\/?(div|p|h[1-6]|table|tr|td|th|ul|ol|li|blockquote|object|br|hr|img|embed|param|pre|script|html|head|body|meta|link|title|area)[^>]*>/g),
2782 /*1*/ new RegExp().compile(/<\/(div|p|h[1-6]|table|tr|td|th|ul|ol|li|blockquote|object|html|head|body|script)( [^>]*)?>/g),//blocklevel closing tag
2783 /*2*/ new RegExp().compile(/<(div|p|h[1-6]|table|tr|td|th|ul|ol|li|blockquote|object|html|head|body|script)( [^>]*)?>/g),//blocklevel opening tag
2784 /*3*/ new RegExp().compile(/<(br|hr|img|embed|param|pre|meta|link|title|area)[^>]*>/g),//singlet tag
2785 /*4*/ new RegExp().compile(/(^|<\/(pre|script)>)(\s|[^\s])*?(<(pre|script)[^>]*>|$)/g),//find content NOT inside pre and script tags
2786 /*5*/ new RegExp().compile(/(<pre[^>]*>)(\s|[^\s])*?(<\/pre>)/g),//find content inside pre tags
2787 /*6*/ new RegExp().compile(/(^|<!--(\s|\S)*?-->)((\s|\S)*?)(?=<!--(\s|\S)*?-->|$)/g),//find content NOT inside comments
2788 /*7*/ new RegExp().compile(/<\/(table|tbody|tr|td|th|ul|ol|object|html|head|body)( [^>]*)?>/g),//blocklevel closing tag
2790 HTMLArea.__nindent = 0;
2791 HTMLArea.__sindent = "";
2792 HTMLArea.__sindentChar = (typeof sindentChar == "undefined") ? " " : sindentChar;
2794 if(HTMLArea.is_gecko) { //moz changes returns into <br> inside <pre> tags
2795 s = s.replace(c[5], function(str){return str.replace(/<br \/>/g,"\n")});
2797 s = s.replace(c[4], function(strn) { //skip pre and script tags
2798 strn = strn.replace(c[6], function(st,$1,$2,$3) { //exclude comments
2799 string = $3.replace(/[\n\r]/gi, " ").replace(/\s+/gi," ").replace(c[0], function(str) {
2800 if (str.match(c[2])) {
2801 var s = "\n" + HTMLArea.__sindent + str;
2802 // blocklevel openingtag - increase indent
2803 HTMLArea.__sindent += HTMLArea.__sindentChar;
2804 ++HTMLArea.__nindent;
2805 return s;
2806 } else if (str.match(c[1])) {
2807 // blocklevel closingtag - decrease indent
2808 --HTMLArea.__nindent;
2809 HTMLArea.__sindent = "";
2810 for (var i=HTMLArea.__nindent;i>0;--i) {
2811 HTMLArea.__sindent += HTMLArea.__sindentChar;
2813 return (str.match(c[7]) ? "\n" + HTMLArea.__sindent : "") + str;
2815 return str; // this won't actually happen
2817 return $1 + string;
2818 });return strn;
2820 if (s.charAt(0) == "\n") {
2821 return s.substring(1, s.length);
2823 s = s.replace(/ *\n/g,'\n');//strip spaces at end of lines
2824 return s;