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