Changeset 659
- Timestamp:
- 01/15/07 14:28:57 (13 years ago)
- Location:
- trunk
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/examples/testbed.html
r649 r659 36 36 37 37 <!-- Load up the actual editor core --> 38 <script type="text/javascript" src="../ XinhaCore.js"></script>38 <script type="text/javascript" src="../htmlarea.js"></script> 39 39 40 40 <script type="text/javascript"> -
trunk/htmlarea.js
r655 r659 1 /* IMPORTANT:2 3 THIS FILE IS A COPY OF XinhaCore.js FOR BACKWARD COMPATIBILITY PURPOSES ONLY4 IF YOU MAKE CHANGES TO THE CODE, DO IT THERE!5 6 */7 1 8 2 /*--------------------------------------:noTabs=true:tabSize=2:indentSize=2:-- 9 -- Xinha (is not htmlArea) - http://xinha.gogo.co.nz/ 10 -- 11 -- Use of Xinha is granted by the terms of the htmlArea License (based on 12 -- BSD license) please read license.txt in this package for details. 13 -- 14 -- Xinha was originally based on work by Mihai Bazon which is: 15 -- Copyright (c) 2003-2004 dynarch.com. 16 -- Copyright (c) 2002-2003 interactivetools.com, inc. 17 -- This copyright notice MUST stay intact for use. 18 -- 19 -- Developers - Coding Style: 20 -- For the sake of not committing needlessly conflicting changes, 21 -- 22 -- * New code to be indented with 2 spaces ("soft tab"). 23 -- * New code preferably uses BSD-Style Bracing 24 -- if ( foo ) 25 -- { 26 -- bar(); 27 -- } 28 -- * Don't change brace styles unless you're working on the non BSD-Style 29 -- area (so we don't get spurious changes in line numbering). 30 -- * Don't change indentation unless you're working on the badly indented 31 -- area (so we don't get spurious changes of large blocks of code). 32 -- * Jedit is the recommended editor, a comment of this format should be 33 -- included in the top 10 lines of the file (see the embedded edit mode) 3 -- COMPATABILITY FILE 4 -- htmlarea.js is now XinhaCore.js 34 5 -- 35 6 -- $HeadURL$ … … 38 9 -- $LastChangedBy$ 39 10 --------------------------------------------------------------------------*/ 40 41 Xinha.version = 42 { 43 'Release' : 'Trunk', 44 'Head' : '$HeadURL$'.replace(/^[^:]*: (.*) \$$/, '$1'), 45 'Date' : '$LastChangedDate$'.replace(/^[^:]*: ([0-9-]*) ([0-9:]*) ([+0-9]*) \((.*)\) \$/, '$4 $2 $3'), 46 'Revision' : '$LastChangedRevision$'.replace(/^[^:]*: (.*) \$$/, '$1'), 47 'RevisionBy': '$LastChangedBy$'.replace(/^[^:]*: (.*) \$$/, '$1') 48 }; 49 11 50 12 if ( typeof _editor_url == "string" ) 51 13 { … … 59 21 } 60 22 61 // make sure we have a language 62 if ( typeof _editor_lang == "string" ) 63 { 64 _editor_lang = _editor_lang.toLowerCase(); 65 } 66 else 67 { 68 _editor_lang = "en"; 69 } 70 71 // skin stylesheet to load 72 if ( typeof _editor_skin !== "string" ) 73 { 74 _editor_skin = ""; 75 } 76 77 var __xinhas = []; 78 79 // browser identification 80 Xinha.agt = navigator.userAgent.toLowerCase(); 81 Xinha.is_ie = ((Xinha.agt.indexOf("msie") != -1) && (Xinha.agt.indexOf("opera") == -1)); 82 Xinha.is_opera = (Xinha.agt.indexOf("opera") != -1); 83 Xinha.is_mac = (Xinha.agt.indexOf("mac") != -1); 84 Xinha.is_mac_ie = (Xinha.is_ie && Xinha.is_mac); 85 Xinha.is_win_ie = (Xinha.is_ie && !Xinha.is_mac); 86 Xinha.is_gecko = (navigator.product == "Gecko"); 87 Xinha.isRunLocally = document.URL.toLowerCase().search(/^file:/) != -1; 88 if ( Xinha.isRunLocally ) 89 { 90 alert('Xinha *must* be installed on a web server. Locally opened files (those that use the "file://" protocol) cannot properly function. Xinha will try to initialize but may not be correctly loaded.'); 91 } 92 93 // Creates a new Xinha object. Tries to replace the textarea with the given 94 // ID with it. 95 function Xinha(textarea, config) 96 { 97 if ( !textarea ) 98 { 99 throw("Tried to create Xinha without textarea specified."); 100 } 101 102 if ( Xinha.checkSupportedBrowser() ) 103 { 104 if ( typeof config == "undefined" ) 105 { 106 this.config = new Xinha.Config(); 107 } 108 else 109 { 110 this.config = config; 111 } 112 this._htmlArea = null; 113 114 if ( typeof textarea != 'object' ) 115 { 116 textarea = Xinha.getElementById('textarea', textarea); 117 } 118 this._textArea = textarea; 119 this._textArea.spellcheck = false; 120 121 // Before we modify anything, get the initial textarea size 122 this._initial_ta_size = 123 { 124 w: textarea.style.width ? textarea.style.width : ( textarea.offsetWidth ? ( textarea.offsetWidth + 'px' ) : ( textarea.cols + 'em') ), 125 h: textarea.style.height ? textarea.style.height : ( textarea.offsetHeight ? ( textarea.offsetHeight + 'px' ) : ( textarea.rows + 'em') ) 126 }; 127 // Create the loading message elements 128 if ( this.config.showLoading ) 129 { 130 // Create and show the main loading message and the sub loading message for details of loading actions 131 // global element 132 var loading_message = document.createElement("div"); 133 loading_message.id = "loading_" + textarea.name; 134 loading_message.className = "loading"; 135 try 136 { 137 // how can i find the real width in pixels without % or em *and* with no visual errors ? 138 // for instance, a textarea with a style="width:100%" and the body padding > 0 result in a horizontal scrollingbar while loading 139 // A few lines above seems to indicate offsetWidth is not always set 140 loading_message.style.width = textarea.offsetWidth + 'px'; 141 } 142 catch (ex) 143 { 144 // offsetWidth seems not set, so let's use this._initial_ta_size.w, but sometimes it may be too huge width 145 loading_message.style.width = this._initial_ta_size.w; 146 } 147 loading_message.style.left = Xinha.findPosX(textarea) + 'px'; 148 loading_message.style.top = (Xinha.findPosY(textarea) + parseInt(this._initial_ta_size.h, 10) / 2) + 'px'; 149 // main static message 150 var loading_main = document.createElement("div"); 151 loading_main.className = "loading_main"; 152 loading_main.id = "loading_main_" + textarea.name; 153 loading_main.appendChild(document.createTextNode(Xinha._lc("Loading in progress. Please wait !"))); 154 // sub dynamic message 155 var loading_sub = document.createElement("div"); 156 loading_sub.className = "loading_sub"; 157 loading_sub.id = "loading_sub_" + textarea.name; 158 loading_sub.appendChild(document.createTextNode(Xinha._lc("Constructing main object"))); 159 loading_message.appendChild(loading_main); 160 loading_message.appendChild(loading_sub); 161 document.body.appendChild(loading_message); 162 this.setLoadingMessage("Constructing object"); 163 } 164 165 this._editMode = "wysiwyg"; 166 this.plugins = {}; 167 this._timerToolbar = null; 168 this._timerUndo = null; 169 this._undoQueue = [this.config.undoSteps]; 170 this._undoPos = -1; 171 this._customUndo = true; 172 this._mdoc = document; // cache the document, we need it in plugins 173 this.doctype = ''; 174 this.__htmlarea_id_num = __xinhas.length; 175 __xinhas[this.__htmlarea_id_num] = this; 176 177 this._notifyListeners = {}; 178 179 // Panels 180 var panels = 181 { 182 right: 183 { 184 on: true, 185 container: document.createElement('td'), 186 panels: [] 187 }, 188 left: 189 { 190 on: true, 191 container: document.createElement('td'), 192 panels: [] 193 }, 194 top: 195 { 196 on: true, 197 container: document.createElement('td'), 198 panels: [] 199 }, 200 bottom: 201 { 202 on: true, 203 container: document.createElement('td'), 204 panels: [] 205 } 206 }; 207 208 for ( var i in panels ) 209 { 210 if(!panels[i].container) { continue; } // prevent iterating over wrong type 211 panels[i].div = panels[i].container; // legacy 212 panels[i].container.className = 'panels ' + i; 213 Xinha.freeLater(panels[i], 'container'); 214 Xinha.freeLater(panels[i], 'div'); 215 } 216 // finally store the variable 217 this._panels = panels; 218 219 Xinha.freeLater(this, '_textArea'); 220 } 221 } 222 223 Xinha.onload = function() { }; 224 Xinha.init = function() { Xinha.onload(); }; 225 226 // cache some regexps 227 Xinha.RE_tagName = /(<\/|<)\s*([^ \t\n>]+)/ig; 228 Xinha.RE_doctype = /(<!doctype((.|\n)*?)>)\n?/i; 229 Xinha.RE_head = /<head>((.|\n)*?)<\/head>/i; 230 Xinha.RE_body = /<body[^>]*>((.|\n|\r|\t)*?)<\/body>/i; 231 Xinha.RE_Specials = /([\/\^$*+?.()|{}[\]])/g; 232 Xinha.RE_email = /[_a-zA-Z\d\-\.]{3,}@[_a-zA-Z\d\-]{2,}(\.[_a-zA-Z\d\-]{2,})+/i; 233 Xinha.RE_url = /(https?:\/\/)?(([a-z0-9_]+:[a-z0-9_]+@)?[a-z0-9_-]{2,}(\.[a-z0-9_-]{2,}){2,}(:[0-9]+)?(\/\S+)*)/i; 234 235 Xinha.Config = function() 236 { 237 var cfg = this; 238 this.version = Xinha.version.Revision; 239 240 // Width and Height 241 // you may set these as follows 242 // width = 'auto' -- the width of the original textarea will be used 243 // width = 'toolbar' -- the width of the toolbar will be used 244 // width = '<css measure>' -- use any css measurement, eg width = '75%' 245 // 246 // height = 'auto' -- the height of the original textarea 247 // height = '<css measure>' -- any css measurement, eg height = '480px' 248 this.width = "auto"; 249 this.height = "auto"; 250 251 // the next parameter specifies whether the toolbar should be included 252 // in the size above, or are extra to it. If false then it's recommended 253 // to have explicit pixel sizes above (or on your textarea and have auto above) 254 this.sizeIncludesBars = true; 255 256 // the next parameter specifies whether the panels should be included 257 // in the size above, or are extra to it. If false then it's recommended 258 // to have explicit pixel sizes above (or on your textarea and have auto above) 259 this.sizeIncludesPanels = true; 260 261 // each of the panels has a dimension, for the left/right it's the width 262 // for the top/bottom it's the height. 263 // 264 // WARNING: PANEL DIMENSIONS MUST BE SPECIFIED AS PIXEL WIDTHS 265 this.panel_dimensions = 266 { 267 left: '200px', // Width 268 right: '200px', 269 top: '100px', // Height 270 bottom: '100px' 271 }; 272 273 // enable creation of a status bar? 274 this.statusBar = true; 275 276 // intercept ^V and use the Xinha paste command 277 // If false, then passes ^V through to browser editor widget 278 this.htmlareaPaste = false; 279 280 this.mozParaHandler = 'best'; // set to 'built-in', 'dirty' or 'best' 281 // built-in: will (may) use 'br' instead of 'p' tags 282 // dirty : will use p and work good enough for the majority of cases, 283 // best : works the best, but it's about 12kb worth of javascript 284 // and will probably be slower than 'dirty'. This is the "EnterParagraphs" 285 // plugin from "hipikat", rolled in to be part of the core code 286 287 // maximum size of the undo queue 288 this.undoSteps = 20; 289 290 // the time interval at which undo samples are taken 291 this.undoTimeout = 500; // 1/2 sec. 292 293 // set this to true if you want to explicitly right-justify when 294 // setting the text direction to right-to-left 295 this.changeJustifyWithDirection = false; 296 297 // if true then Xinha will retrieve the full HTML, starting with the 298 // <HTML> tag. 299 this.fullPage = false; 300 301 // style included in the iframe document 302 this.pageStyle = ""; 303 304 // external stylesheets to load (REFERENCE THESE ABSOLUTELY) 305 this.pageStyleSheets = []; 306 307 // specify a base href for relative links 308 this.baseHref = null; 309 310 // when the editor is in different directory depth as the edited page relative image sources 311 // will break the display of your images 312 // this fixes an issue where Mozilla converts the urls of images and links that are on the same server 313 // to relative ones (../) when dragging them around in the editor (Ticket #448) 314 this.expandRelativeUrl = true; 315 316 // we can strip the base href out of relative links to leave them relative, reason for this 317 // especially if you don't specify a baseHref is that mozilla at least (& IE ?) will prefix 318 // the baseHref to any relative links to make them absolute, which isn't what you want most the time. 319 this.stripBaseHref = true; 320 321 // and we can strip the url of the editor page from named links (eg <a href="#top">...</a>) 322 // reason for this is that mozilla at least (and IE ?) prefixes location.href to any 323 // that don't have a url prefixing them 324 this.stripSelfNamedAnchors = true; 325 326 // sometimes high-ascii in links can cause problems for servers (basically they don't recognise them) 327 // so you can use this flag to ensure that all characters other than the normal ascii set (actually 328 // only ! through ~) are escaped in URLs to % codes 329 this.only7BitPrintablesInURLs = true; 330 331 // if you are putting the HTML written in Xinha into an email you might want it to be 7-bit 332 // characters only. This config option (off by default) will convert all characters consuming 333 // more than 7bits into UNICODE decimal entity references (actually it will convert anything 334 // below <space> (chr 20) except cr, lf and tab and above <tilde> (~, chr 7E)) 335 this.sevenBitClean = false; 336 337 // sometimes we want to be able to replace some string in the html comng in and going out 338 // so that in the editor we use the "internal" string, and outside and in the source view 339 // we use the "external" string this is useful for say making special codes for 340 // your absolute links, your external string might be some special code, say "{server_url}" 341 // an you say that the internal represenattion of that should be http://your.server/ 342 this.specialReplacements = {}; // { 'external_string' : 'internal_string' } 343 344 // set to true if you want Word code to be cleaned upon Paste 345 this.killWordOnPaste = true; 346 347 // enable the 'Target' field in the Make Link dialog 348 this.makeLinkShowsTarget = true; 349 350 // CharSet of the iframe, default is the charset of the document 351 this.charSet = Xinha.is_gecko ? document.characterSet : document.charset; 352 353 // URL-s 354 this.imgURL = "images/"; 355 this.popupURL = "popups/"; 356 357 // remove tags (these have to be a regexp, or null if this functionality is not desired) 358 this.htmlRemoveTags = null; 359 360 // Turning this on will turn all "linebreak" and "separator" items in your toolbar into soft-breaks, 361 // this means that if the items between that item and the next linebreak/separator can 362 // fit on the same line as that which came before then they will, otherwise they will 363 // float down to the next line. 364 365 // If you put a linebreak and separator next to each other, only the separator will 366 // take effect, this allows you to have one toolbar that works for both flowToolbars = true and false 367 // infact the toolbar below has been designed in this way, if flowToolbars is false then it will 368 // create explictly two lines (plus any others made by plugins) breaking at justifyleft, however if 369 // flowToolbars is false and your window is narrow enough then it will create more than one line 370 // even neater, if you resize the window the toolbars will reflow. Niiiice. 371 372 this.flowToolbars = true; 373 374 // set to true if you want the loading panel to show at startup 375 this.showLoading = false; 376 377 // set to false if you want to allow JavaScript in the content, otherwise <script> tags are stripped out 378 this.stripScripts = true; 379 380 // see if the text just typed looks like a URL, or email address 381 // and link it appropriatly 382 this.convertUrlsToLinks = false; 383 384 // size of color picker cells 385 this.colorPickerCellSize = '6px'; 386 // granularity of color picker cells (number per column/row) 387 this.colorPickerGranularity = 18; 388 // position of color picker from toolbar button 389 this.colorPickerPosition = 'bottom,right'; 390 // set to true to show websafe checkbox in picker 391 this.colorPickerWebSafe = false; 392 // number of recent colors to remember 393 this.colorPickerSaveColors = 20; 394 395 /** CUSTOMIZING THE TOOLBAR 396 * ------------------------- 397 * 398 * It is recommended that you customize the toolbar contents in an 399 * external file (i.e. the one calling Xinha) and leave this one 400 * unchanged. That's because when we (InteractiveTools.com) release a 401 * new official version, it's less likely that you will have problems 402 * upgrading Xinha. 403 */ 404 this.toolbar = 405 [ 406 ["popupeditor"], 407 ["separator","formatblock","fontname","fontsize","bold","italic","underline","strikethrough"], 408 ["separator","forecolor","hilitecolor","textindicator"], 409 ["separator","subscript","superscript"], 410 ["linebreak","separator","justifyleft","justifycenter","justifyright","justifyfull"], 411 ["separator","insertorderedlist","insertunorderedlist","outdent","indent"], 412 ["separator","inserthorizontalrule","createlink","insertimage","inserttable"], 413 ["linebreak","separator","undo","redo","selectall","print"], (Xinha.is_gecko ? [] : ["cut","copy","paste","overwrite","saveas"]), 414 ["separator","killword","clearfonts","removeformat","toggleborders","splitblock","lefttoright", "righttoleft"], 415 ["separator","htmlmode","showhelp","about"] 416 ]; 417 418 419 this.fontname = 420 { 421 "— font —": '', 422 "Arial": 'arial,helvetica,sans-serif', 423 "Courier New": 'courier new,courier,monospace', 424 "Georgia": 'georgia,times new roman,times,serif', 425 "Tahoma": 'tahoma,arial,helvetica,sans-serif', 426 "Times New Roman": 'times new roman,times,serif', 427 "Verdana": 'verdana,arial,helvetica,sans-serif', 428 "impact": 'impact', 429 "WingDings": 'wingdings' 430 }; 431 432 this.fontsize = 433 { 434 "— size —": "", 435 "1 (8 pt)" : "1", 436 "2 (10 pt)": "2", 437 "3 (12 pt)": "3", 438 "4 (14 pt)": "4", 439 "5 (18 pt)": "5", 440 "6 (24 pt)": "6", 441 "7 (36 pt)": "7" 442 }; 443 444 this.formatblock = 445 { 446 "— format —": "", 447 "Heading 1": "h1", 448 "Heading 2": "h2", 449 "Heading 3": "h3", 450 "Heading 4": "h4", 451 "Heading 5": "h5", 452 "Heading 6": "h6", 453 "Normal" : "p", 454 "Address" : "address", 455 "Formatted": "pre" 456 }; 457 458 this.customSelects = {}; 459 460 function cut_copy_paste(e, cmd, obj) { e.execCommand(cmd); } 461 462 this.debug = true; 463 464 this.URIs = 465 { 466 "blank": "popups/blank.html", 467 "link": "link.html", 468 "insert_image": "insert_image.html", 469 "insert_table": "insert_table.html", 470 "select_color": "select_color.html", 471 "about": "about.html", 472 "help": "editor_help.html" 473 }; 474 475 476 // ADDING CUSTOM BUTTONS: please read below! 477 // format of the btnList elements is "ID: [ ToolTip, Icon, Enabled in text mode?, ACTION ]" 478 // - ID: unique ID for the button. If the button calls document.execCommand 479 // it's wise to give it the same name as the called command. 480 // - ACTION: function that gets called when the button is clicked. 481 // it has the following prototype: 482 // function(editor, buttonName) 483 // - editor is the Xinha object that triggered the call 484 // - buttonName is the ID of the clicked button 485 // These 2 parameters makes it possible for you to use the same 486 // handler for more Xinha objects or for more different buttons. 487 // - ToolTip: tooltip, will be translated below 488 // - Icon: path to an icon image file for the button 489 // OR; you can use an 18x18 block of a larger image by supllying an array 490 // that has three elemtents, the first is the larger image, the second is the column 491 // the third is the row. The ros and columns numbering starts at 0 but there is 492 // a header row and header column which have numbering to make life easier. 493 // See images/buttons_main.gif to see how it's done. 494 // - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time. 495 this.btnList = 496 { 497 bold: [ "Bold", Xinha._lc({key: 'button_bold', string: ["ed_buttons_main.gif",3,2]}, 'Xinha'), false, function(e) { e.execCommand("bold"); } ], 498 italic: [ "Italic", Xinha._lc({key: 'button_italic', string: ["ed_buttons_main.gif",2,2]}, 'Xinha'), false, function(e) { e.execCommand("italic"); } ], 499 underline: [ "Underline", Xinha._lc({key: 'button_underline', string: ["ed_buttons_main.gif",2,0]}, 'Xinha'), false, function(e) { e.execCommand("underline"); } ], 500 strikethrough: [ "Strikethrough", Xinha._lc({key: 'button_strikethrough', string: ["ed_buttons_main.gif",3,0]}, 'Xinha'), false, function(e) { e.execCommand("strikethrough"); } ], 501 subscript: [ "Subscript", Xinha._lc({key: 'button_subscript', string: ["ed_buttons_main.gif",3,1]}, 'Xinha'), false, function(e) { e.execCommand("subscript"); } ], 502 superscript: [ "Superscript", Xinha._lc({key: 'button_superscript', string: ["ed_buttons_main.gif",2,1]}, 'Xinha'), false, function(e) { e.execCommand("superscript"); } ], 503 504 justifyleft: [ "Justify Left", ["ed_buttons_main.gif",0,0], false, function(e) { e.execCommand("justifyleft"); } ], 505 justifycenter: [ "Justify Center", ["ed_buttons_main.gif",1,1], false, function(e){ e.execCommand("justifycenter"); } ], 506 justifyright: [ "Justify Right", ["ed_buttons_main.gif",1,0], false, function(e) { e.execCommand("justifyright"); } ], 507 justifyfull: [ "Justify Full", ["ed_buttons_main.gif",0,1], false, function(e) { e.execCommand("justifyfull"); } ], 508 509 orderedlist: [ "Ordered List", ["ed_buttons_main.gif",0,3], false, function(e) { e.execCommand("insertorderedlist"); } ], 510 unorderedlist: [ "Bulleted List", ["ed_buttons_main.gif",1,3], false, function(e) { e.execCommand("insertunorderedlist"); } ], 511 insertorderedlist: [ "Ordered List", ["ed_buttons_main.gif",0,3], false, function(e) { e.execCommand("insertorderedlist"); } ], 512 insertunorderedlist: [ "Bulleted List", ["ed_buttons_main.gif",1,3], false, function(e) { e.execCommand("insertunorderedlist"); } ], 513 514 outdent: [ "Decrease Indent", ["ed_buttons_main.gif",1,2], false, function(e) { e.execCommand("outdent"); } ], 515 indent: [ "Increase Indent",["ed_buttons_main.gif",0,2], false, function(e) { e.execCommand("indent"); } ], 516 forecolor: [ "Font Color", ["ed_buttons_main.gif",3,3], false, function(e) { e.execCommand("forecolor"); } ], 517 hilitecolor: [ "Background Color", ["ed_buttons_main.gif",2,3], false, function(e) { e.execCommand("hilitecolor"); } ], 518 519 undo: [ "Undoes your last action", ["ed_buttons_main.gif",4,2], false, function(e) { e.execCommand("undo"); } ], 520 redo: [ "Redoes your last action", ["ed_buttons_main.gif",5,2], false, function(e) { e.execCommand("redo"); } ], 521 cut: [ "Cut selection", ["ed_buttons_main.gif",5,0], false, cut_copy_paste ], 522 copy: [ "Copy selection", ["ed_buttons_main.gif",4,0], false, cut_copy_paste ], 523 paste: [ "Paste from clipboard", ["ed_buttons_main.gif",4,1], false, cut_copy_paste ], 524 selectall: [ "Select all", "ed_selectall.gif", false, function(e) {e.execCommand("selectall");} ], 525 526 inserthorizontalrule: [ "Horizontal Rule", ["ed_buttons_main.gif",6,0], false, function(e) { e.execCommand("inserthorizontalrule"); } ], 527 createlink: [ "Insert Web Link", ["ed_buttons_main.gif",6,1], false, function(e) { e._createLink(); } ], 528 insertimage: [ "Insert/Modify Image", ["ed_buttons_main.gif",6,3], false, function(e) { e.execCommand("insertimage"); } ], 529 inserttable: [ "Insert Table", ["ed_buttons_main.gif",6,2], false, function(e) { e.execCommand("inserttable"); } ], 530 531 htmlmode: [ "Toggle HTML Source", ["ed_buttons_main.gif",7,0], true, function(e) { e.execCommand("htmlmode"); } ], 532 toggleborders: [ "Toggle Borders", ["ed_buttons_main.gif",7,2], false, function(e) { e._toggleBorders(); } ], 533 print: [ "Print document", ["ed_buttons_main.gif",8,1], false, function(e) { if(Xinha.is_gecko) {e._iframe.contentWindow.print(); } else { e.focusEditor(); print(); } } ], 534 saveas: [ "Save as", "ed_saveas.gif", false, function(e) { e.execCommand("saveas",false,"noname.htm"); } ], 535 about: [ "About this editor", ["ed_buttons_main.gif",8,2], true, function(e) { e.execCommand("about"); } ], 536 showhelp: [ "Help using editor", ["ed_buttons_main.gif",9,2], true, function(e) { e.execCommand("showhelp"); } ], 537 538 splitblock: [ "Split Block", "ed_splitblock.gif", false, function(e) { e._splitBlock(); } ], 539 lefttoright: [ "Direction left to right", ["ed_buttons_main.gif",0,4], false, function(e) { e.execCommand("lefttoright"); } ], 540 righttoleft: [ "Direction right to left", ["ed_buttons_main.gif",1,4], false, function(e) { e.execCommand("righttoleft"); } ], 541 overwrite: [ "Insert/Overwrite", "ed_overwrite.gif", false, function(e) { e.execCommand("overwrite"); } ], 542 543 wordclean: [ "MS Word Cleaner", ["ed_buttons_main.gif",5,3], false, function(e) { e._wordClean(); } ], 544 clearfonts: [ "Clear Inline Font Specifications", ["ed_buttons_main.gif",5,4], true, function(e) { e._clearFonts(); } ], 545 removeformat: [ "Remove formatting", ["ed_buttons_main.gif",4,4], false, function(e) { e.execCommand("removeformat"); } ], 546 killword: [ "Clear MSOffice tags", ["ed_buttons_main.gif",4,3], false, function(e) { e.execCommand("killword"); } ] 547 }; 548 549 /* ADDING CUSTOM BUTTONS 550 * --------------------- 551 * 552 * It is recommended that you add the custom buttons in an external 553 * file and leave this one unchanged. That's because when we 554 * (InteractiveTools.com) release a new official version, it's less 555 * likely that you will have problems upgrading Xinha. 556 * 557 * Example on how to add a custom button when you construct the Xinha: 558 * 559 * var editor = new Xinha("your_text_area_id"); 560 * var cfg = editor.config; // this is the default configuration 561 * cfg.btnList["my-hilite"] = 562 * [ function(editor) { editor.surroundHTML('<span style="background:yellow">', '</span>'); }, // action 563 * "Highlight selection", // tooltip 564 * "my_hilite.gif", // image 565 * false // disabled in text mode 566 * ]; 567 * cfg.toolbar.push(["linebreak", "my-hilite"]); // add the new button to the toolbar 568 * 569 * An alternate (also more convenient and recommended) way to 570 * accomplish this is to use the registerButton function below. 571 */ 572 // initialize tooltips from the I18N module and generate correct image path 573 for ( var i in this.btnList ) 574 { 575 var btn = this.btnList[i]; 576 // prevent iterating over wrong type 577 if ( typeof btn != 'object' ) 578 { 579 continue; 580 } 581 if ( typeof btn[1] != 'string' ) 582 { 583 btn[1][0] = _editor_url + this.imgURL + btn[1][0]; 584 } 585 else 586 { 587 btn[1] = _editor_url + this.imgURL + btn[1]; 588 } 589 btn[0] = Xinha._lc(btn[0]); //initialize tooltip 590 } 591 592 }; 593 594 /** Helper function: register a new button with the configuration. It can be 595 * called with all 5 arguments, or with only one (first one). When called with 596 * only one argument it must be an object with the following properties: id, 597 * tooltip, image, textMode, action. Examples: 598 * 599 * 1. config.registerButton("my-hilite", "Hilite text", "my-hilite.gif", false, function(editor) {...}); 600 * 2. config.registerButton({ 601 * id : "my-hilite", // the ID of your button 602 * tooltip : "Hilite text", // the tooltip 603 * image : "my-hilite.gif", // image to be displayed in the toolbar 604 * textMode : false, // disabled in text mode 605 * action : function(editor) { // called when the button is clicked 606 * editor.surroundHTML('<span class="hilite">', '</span>'); 607 * }, 608 * context : "p" // will be disabled if outside a <p> element 609 * }); 610 */ 611 Xinha.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context) 612 { 613 var the_id; 614 if ( typeof id == "string" ) 615 { 616 the_id = id; 617 } 618 else if ( typeof id == "object" ) 619 { 620 the_id = id.id; 621 } 622 else 623 { 624 alert("ERROR [Xinha.Config::registerButton]:\ninvalid arguments"); 625 return false; 626 } 627 // check for existing id 628 // if(typeof this.customSelects[the_id] != "undefined") 629 // { 630 // alert("WARNING [Xinha.Config::registerDropdown]:\nA dropdown with the same ID already exists."); 631 // } 632 // if(typeof this.btnList[the_id] != "undefined") { 633 // alert("WARNING [Xinha.Config::registerDropdown]:\nA button with the same ID already exists."); 634 // } 635 switch ( typeof id ) 636 { 637 case "string": 638 this.btnList[id] = [ tooltip, image, textMode, action, context ]; 639 break; 640 case "object": 641 this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; 642 break; 643 } 644 }; 645 646 Xinha.prototype.registerPanel = function(side, object) 647 { 648 if ( !side ) 649 { 650 side = 'right'; 651 } 652 this.setLoadingMessage('Register panel ' + side); 653 var panel = this.addPanel(side); 654 if ( object ) 655 { 656 object.drawPanelIn(panel); 657 } 658 }; 659 660 /** The following helper function registers a dropdown box with the editor 661 * configuration. You still have to add it to the toolbar, same as with the 662 * buttons. Call it like this: 663 * 664 * FIXME: add example 665 */ 666 Xinha.Config.prototype.registerDropdown = function(object) 667 { 668 // check for existing id 669 // if ( typeof this.customSelects[object.id] != "undefined" ) 670 // { 671 // alert("WARNING [Xinha.Config::registerDropdown]:\nA dropdown with the same ID already exists."); 672 // } 673 // if ( typeof this.btnList[object.id] != "undefined" ) 674 // { 675 // alert("WARNING [Xinha.Config::registerDropdown]:\nA button with the same ID already exists."); 676 // } 677 this.customSelects[object.id] = object; 678 }; 679 680 /** Call this function to remove some buttons/drop-down boxes from the toolbar. 681 * Pass as the only parameter a string containing button/drop-down names 682 * delimited by spaces. Note that the string should also begin with a space 683 * and end with a space. Example: 684 * 685 * config.hideSomeButtons(" fontname fontsize textindicator "); 686 * 687 * It's useful because it's easier to remove stuff from the defaul toolbar than 688 * create a brand new toolbar ;-) 689 */ 690 Xinha.Config.prototype.hideSomeButtons = function(remove) 691 { 692 var toolbar = this.toolbar; 693 for ( var i = toolbar.length; --i >= 0; ) 694 { 695 var line = toolbar[i]; 696 for ( var j = line.length; --j >= 0; ) 697 { 698 if ( remove.indexOf(" " + line[j] + " ") >= 0 ) 699 { 700 var len = 1; 701 if ( /separator|space/.test(line[j + 1]) ) 702 { 703 len = 2; 704 } 705 line.splice(j, len); 706 } 707 } 708 } 709 }; 710 711 /** Helper Function: add buttons/drop-downs boxes with title or separator to the toolbar 712 * if the buttons/drop-downs boxes doesn't allready exists. 713 * id: button or selectbox (as array with separator or title) 714 * where: button or selectbox (as array if the first is not found take the second and so on) 715 * position: 716 * -1 = insert button (id) one position before the button (where) 717 * 0 = replace button (where) by button (id) 718 * +1 = insert button (id) one position after button (where) 719 * 720 * cfg.addToolbarElement(["T[title]", "button_id", "separator"] , ["first_id","second_id"], -1); 721 */ 722 723 Xinha.Config.prototype.addToolbarElement = function(id, where, position) 724 { 725 var toolbar = this.toolbar; 726 var a, i, j, o, sid; 727 var idIsArray = false; 728 var whereIsArray = false; 729 var whereLength = 0; 730 var whereJ = 0; 731 var whereI = 0; 732 var exists = false; 733 var found = false; 734 // check if id and where are arrys 735 if ( ( id && typeof id == "object" ) && ( id.constructor == Array ) ) 736 { 737 idIsArray = true; 738 } 739 if ( ( where && typeof where == "object" ) && ( where.constructor == Array ) ) 740 { 741 whereIsArray = true; 742 whereLength = where.length; 743 } 744 745 if ( idIsArray ) //find the button/select box in input array 746 { 747 for ( i = 0; i < id.length; ++i ) 748 { 749 if ( ( id[i] != "separator" ) && ( id[i].indexOf("T[") !== 0) ) 750 { 751 sid = id[i]; 752 } 753 } 754 } 755 else 756 { 757 sid = id; 758 } 759 760 for ( i = 0; !exists && !found && i < toolbar.length; ++i ) 761 { 762 a = toolbar[i]; 763 for ( j = 0; !found && j < a.length; ++j ) 764 { 765 // check if button/select box exists 766 if ( a[i] == sid ) 767 { 768 exists = true; 769 break; 770 } 771 if ( whereIsArray ) 772 { 773 for ( o = 0; o < whereLength; ++o ) 774 { 775 if ( a[j] == where[o] ) 776 { 777 if ( o === 0 ) 778 { 779 found = true; 780 j--; 781 break; 782 } 783 else 784 { 785 whereI = i; 786 whereJ = j; 787 whereLength = o; 788 } 789 } 790 } 791 } 792 else 793 { 794 // find the position to insert 795 if ( a[j] == where ) 796 { 797 found = true; 798 break; 799 } 800 } 801 } 802 } 803 804 if ( !exists ) 805 { 806 //if check found any other as the first button 807 if ( !found && whereIsArray ) 808 { 809 if ( where.length != whereLength ) 810 { 811 j = whereJ; 812 a = toolbar[whereI]; 813 found = true; 814 } 815 } 816 if ( found ) 817 { 818 // replace the found button 819 if ( position === 0 ) 820 { 821 if ( idIsArray) 822 { 823 a[j] = id[id.length-1]; 824 for ( i = id.length-1; --i >= 0; ) 825 { 826 a.splice(j, 0, id[i]); 827 } 828 } 829 else 830 { 831 a[j] = id; 832 } 833 } 834 else 835 { 836 // insert before/after the found button 837 if ( position < 0 ) 838 { 839 j = j + position + 1; //correct position before 840 } 841 else if ( position > 0 ) 842 { 843 j = j + position; //correct posion after 844 } 845 if ( idIsArray ) 846 { 847 for ( i = id.length; --i >= 0; ) 848 { 849 a.splice(j, 0, id[i]); 850 } 851 } 852 else 853 { 854 a.splice(j, 0, id); 855 } 856 } 857 } 858 else 859 { 860 // no button found 861 toolbar[0].splice(0, 0, "separator"); 862 if ( idIsArray) 863 { 864 for ( i = id.length; --i >= 0; ) 865 { 866 toolbar[0].splice(0, 0, id[i]); 867 } 868 } 869 else 870 { 871 toolbar[0].splice(0, 0, id); 872 } 873 } 874 } 875 }; 876 877 Xinha.Config.prototype.removeToolbarElement = Xinha.Config.prototype.hideSomeButtons; 878 879 /** Helper function: replace all TEXTAREA-s in the document with Xinha-s. */ 880 Xinha.replaceAll = function(config) 881 { 882 var tas = document.getElementsByTagName("textarea"); 883 // @todo: weird syntax, doesnt help to read the code, doesnt obfuscate it and doesnt make it quicker, better rewrite this part 884 for ( var i = tas.length; i > 0; (new Xinha(tas[--i], config)).generate() ) 885 { 886 // NOP 887 } 888 }; 889 890 /** Helper function: replaces the TEXTAREA with the given ID with Xinha. */ 891 Xinha.replace = function(id, config) 892 { 893 var ta = Xinha.getElementById("textarea", id); 894 return ta ? (new Xinha(ta, config)).generate() : null; 895 }; 896 897 // Creates the toolbar and appends it to the _htmlarea 898 Xinha.prototype._createToolbar = function () 899 { 900 this.setLoadingMessage('Create Toolbar'); 901 var editor = this; // to access this in nested functions 902 903 var toolbar = document.createElement("div"); 904 // ._toolbar is for legacy, ._toolBar is better thanks. 905 this._toolBar = this._toolbar = toolbar; 906 toolbar.className = "toolbar"; 907 toolbar.unselectable = "1"; 908 909 Xinha.freeLater(this, '_toolBar'); 910 Xinha.freeLater(this, '_toolbar'); 911 912 var tb_row = null; 913 var tb_objects = {}; 914 this._toolbarObjects = tb_objects; 915 916 this._createToolbar1(editor, toolbar, tb_objects); 917 this._htmlArea.appendChild(toolbar); 918 919 return toolbar; 920 }; 921 922 // FIXME : function never used, can probably be removed from source 923 Xinha.prototype._setConfig = function(config) 924 { 925 this.config = config; 926 }; 927 928 Xinha.prototype._addToolbar = function() 929 { 930 this._createToolbar1(this, this._toolbar, this._toolbarObjects); 931 }; 932 933 /** 934 * Create a break element to add in the toolbar 935 * 936 * @return {Object} HTML element to add 937 * @private 938 */ 939 Xinha._createToolbarBreakingElement = function() 940 { 941 var brk = document.createElement('div'); 942 brk.style.height = '1px'; 943 brk.style.width = '1px'; 944 brk.style.lineHeight = '1px'; 945 brk.style.fontSize = '1px'; 946 brk.style.clear = 'both'; 947 return brk; 948 }; 949 950 // separate from previous createToolBar to allow dynamic change of toolbar 951 Xinha.prototype._createToolbar1 = function (editor, toolbar, tb_objects) 952 { 953 var tb_row; 954 // This shouldn't be necessary, but IE seems to float outside of the container 955 // when we float toolbar sections, so we have to clear:both here as well 956 // as at the end (which we do have to do). 957 if ( editor.config.flowToolbars ) 958 { 959 toolbar.appendChild(Xinha._createToolbarBreakingElement()); 960 } 961 962 // creates a new line in the toolbar 963 function newLine() 964 { 965 if ( typeof tb_row != 'undefined' && tb_row.childNodes.length === 0) 966 { 967 return; 968 } 969 970 var table = document.createElement("table"); 971 table.border = "0px"; 972 table.cellSpacing = "0px"; 973 table.cellPadding = "0px"; 974 if ( editor.config.flowToolbars ) 975 { 976 if ( Xinha.is_ie ) 977 { 978 table.style.styleFloat = "left"; 979 } 980 else 981 { 982 table.style.cssFloat = "left"; 983 } 984 } 985 986 toolbar.appendChild(table); 987 // TBODY is required for IE, otherwise you don't see anything 988 // in the TABLE. 989 var tb_body = document.createElement("tbody"); 990 table.appendChild(tb_body); 991 tb_row = document.createElement("tr"); 992 tb_body.appendChild(tb_row); 993 994 table.className = 'toolbarRow'; // meh, kinda. 995 } // END of function: newLine 996 997 // init first line 998 newLine(); 999 1000 // updates the state of a toolbar element. This function is member of 1001 // a toolbar element object (unnamed objects created by createButton or 1002 // createSelect functions below). 1003 function setButtonStatus(id, newval) 1004 { 1005 var oldval = this[id]; 1006 var el = this.element; 1007 if ( oldval != newval ) 1008 { 1009 switch (id) 1010 { 1011 case "enabled": 1012 if ( newval ) 1013 { 1014 Xinha._removeClass(el, "buttonDisabled"); 1015 el.disabled = false; 1016 } 1017 else 1018 { 1019 Xinha._addClass(el, "buttonDisabled"); 1020 el.disabled = true; 1021 } 1022 break; 1023 case "active": 1024 if ( newval ) 1025 { 1026 Xinha._addClass(el, "buttonPressed"); 1027 } 1028 else 1029 { 1030 Xinha._removeClass(el, "buttonPressed"); 1031 } 1032 break; 1033 } 1034 this[id] = newval; 1035 } 1036 } // END of function: setButtonStatus 1037 1038 // this function will handle creation of combo boxes. Receives as 1039 // parameter the name of a button as defined in the toolBar config. 1040 // This function is called from createButton, above, if the given "txt" 1041 // doesn't match a button. 1042 function createSelect(txt) 1043 { 1044 var options = null; 1045 var el = null; 1046 var cmd = null; 1047 var customSelects = editor.config.customSelects; 1048 var context = null; 1049 var tooltip = ""; 1050 switch (txt) 1051 { 1052 case "fontsize": 1053 case "fontname": 1054 case "formatblock": 1055 // the following line retrieves the correct 1056 // configuration option because the variable name 1057 // inside the Config object is named the same as the 1058 // button/select in the toolbar. For instance, if txt 1059 // == "formatblock" we retrieve config.formatblock (or 1060 // a different way to write it in JS is 1061 // config["formatblock"]. 1062 options = editor.config[txt]; 1063 cmd = txt; 1064 break; 1065 default: 1066 // try to fetch it from the list of registered selects 1067 cmd = txt; 1068 var dropdown = customSelects[cmd]; 1069 if ( typeof dropdown != "undefined" ) 1070 { 1071 options = dropdown.options; 1072 context = dropdown.context; 1073 if ( typeof dropdown.tooltip != "undefined" ) 1074 { 1075 tooltip = dropdown.tooltip; 1076 } 1077 } 1078 else 1079 { 1080 alert("ERROR [createSelect]:\nCan't find the requested dropdown definition"); 1081 } 1082 break; 1083 } 1084 if ( options ) 1085 { 1086 el = document.createElement("select"); 1087 el.title = tooltip; 1088 var obj = 1089 { 1090 name : txt, // field name 1091 element : el, // the UI element (SELECT) 1092 enabled : true, // is it enabled? 1093 text : false, // enabled in text mode? 1094 cmd : cmd, // command ID 1095 state : setButtonStatus, // for changing state 1096 context : context 1097 }; 1098 1099 Xinha.freeLater(obj); 1100 1101 tb_objects[txt] = obj; 1102 1103 for ( var i in options ) 1104 { 1105 // prevent iterating over wrong type 1106 if ( typeof(options[i]) != 'string' ) 1107 { 1108 continue; 1109 } 1110 var op = document.createElement("option"); 1111 op.innerHTML = Xinha._lc(i); 1112 op.value = options[i]; 1113 el.appendChild(op); 1114 } 1115 Xinha._addEvent(el, "change", function () { editor._comboSelected(el, txt); } ); 1116 } 1117 return el; 1118 } // END of function: createSelect 1119 1120 // appends a new button to toolbar 1121 function createButton(txt) 1122 { 1123 // the element that will be created 1124 var el, btn, obj = null; 1125 switch (txt) 1126 { 1127 case "separator": 1128 if ( editor.config.flowToolbars ) 1129 { 1130 newLine(); 1131 } 1132 el = document.createElement("div"); 1133 el.className = "separator"; 1134 break; 1135 case "space": 1136 el = document.createElement("div"); 1137 el.className = "space"; 1138 break; 1139 case "linebreak": 1140 newLine(); 1141 return false; 1142 case "textindicator": 1143 el = document.createElement("div"); 1144 el.appendChild(document.createTextNode("A")); 1145 el.className = "indicator"; 1146 el.title = Xinha._lc("Current style"); 1147 obj = 1148 { 1149 name : txt, // the button name (i.e. 'bold') 1150 element : el, // the UI element (DIV) 1151 enabled : true, // is it enabled? 1152 active : false, // is it pressed? 1153 text : false, // enabled in text mode? 1154 cmd : "textindicator", // the command ID 1155 state : setButtonStatus // for changing state 1156 }; 1157 1158 Xinha.freeLater(obj); 1159 1160 tb_objects[txt] = obj; 1161 break; 1162 default: 1163 btn = editor.config.btnList[txt]; 1164 } 1165 if ( !el && btn ) 1166 { 1167 el = document.createElement("a"); 1168 el.style.display = 'block'; 1169 el.href = 'javascript:void(0)'; 1170 el.style.textDecoration = 'none'; 1171 el.title = btn[0]; 1172 el.className = "button"; 1173 el.style.direction = "ltr"; 1174 // let's just pretend we have a button object, and 1175 // assign all the needed information to it. 1176 obj = 1177 { 1178 name : txt, // the button name (i.e. 'bold') 1179 element : el, // the UI element (DIV) 1180 enabled : true, // is it enabled? 1181 active : false, // is it pressed? 1182 text : btn[2], // enabled in text mode? 1183 cmd : btn[3], // the command ID 1184 state : setButtonStatus, // for changing state 1185 context : btn[4] || null // enabled in a certain context? 1186 }; 1187 Xinha.freeLater(el); 1188 Xinha.freeLater(obj); 1189 1190 tb_objects[txt] = obj; 1191 1192 // prevent drag&drop of the icon to content area 1193 el.ondrag = function() { return false; }; 1194 1195 // handlers to emulate nice flat toolbar buttons 1196 Xinha._addEvent( 1197 el, 1198 "mouseout", 1199 function(ev) 1200 { 1201 if ( obj.enabled ) 1202 { 1203 //Xinha._removeClass(el, "buttonHover"); 1204 Xinha._removeClass(el, "buttonActive"); 1205 if ( obj.active ) 1206 { 1207 Xinha._addClass(el, "buttonPressed"); 1208 } 1209 } 1210 } 1211 ); 1212 1213 Xinha._addEvent( 1214 el, 1215 "mousedown", 1216 function(ev) 1217 { 1218 if ( obj.enabled ) 1219 { 1220 Xinha._addClass(el, "buttonActive"); 1221 Xinha._removeClass(el, "buttonPressed"); 1222 Xinha._stopEvent(Xinha.is_ie ? window.event : ev); 1223 } 1224 } 1225 ); 1226 1227 // when clicked, do the following: 1228 Xinha._addEvent( 1229 el, 1230 "click", 1231 function(ev) 1232 { 1233 if ( obj.enabled ) 1234 { 1235 Xinha._removeClass(el, "buttonActive"); 1236 //Xinha._removeClass(el, "buttonHover"); 1237 if ( Xinha.is_gecko ) 1238 { 1239 editor.activateEditor(); 1240 } 1241 obj.cmd(editor, obj.name, obj); 1242 Xinha._stopEvent(Xinha.is_ie ? window.event : ev); 1243 } 1244 } 1245 ); 1246 1247 var i_contain = Xinha.makeBtnImg(btn[1]); 1248 var img = i_contain.firstChild; 1249 el.appendChild(i_contain); 1250 1251 obj.imgel = img; 1252 obj.swapImage = function(newimg) 1253 { 1254 if ( typeof newimg != 'string' ) 1255 { 1256 img.src = newimg[0]; 1257 img.style.position = 'relative'; 1258 img.style.top = newimg[2] ? ('-' + (18 * (newimg[2] + 1)) + 'px') : '-18px'; 1259 img.style.left = newimg[1] ? ('-' + (18 * (newimg[1] + 1)) + 'px') : '-18px'; 1260 } 1261 else 1262 { 1263 obj.imgel.src = newimg; 1264 img.style.top = '0px'; 1265 img.style.left = '0px'; 1266 } 1267 }; 1268 1269 } 1270 else if( !el ) 1271 { 1272 el = createSelect(txt); 1273 } 1274 1275 return el; 1276 } 1277 1278 var first = true; 1279 for ( var i = 0; i < this.config.toolbar.length; ++i ) 1280 { 1281 if ( !first ) 1282 { 1283 // createButton("linebreak"); 1284 } 1285 else 1286 { 1287 first = false; 1288 } 1289 if ( this.config.toolbar[i] === null ) 1290 { 1291 this.config.toolbar[i] = ['separator']; 1292 } 1293 var group = this.config.toolbar[i]; 1294 1295 for ( var j = 0; j < group.length; ++j ) 1296 { 1297 var code = group[j]; 1298 var tb_cell; 1299 if ( /^([IT])\[(.*?)\]/.test(code) ) 1300 { 1301 // special case, create text label 1302 var l7ed = RegExp.$1 == "I"; // localized? 1303 var label = RegExp.$2; 1304 if ( l7ed ) 1305 { 1306 label = Xinha._lc(label); 1307 } 1308 tb_cell = document.createElement("td"); 1309 tb_row.appendChild(tb_cell); 1310 tb_cell.className = "label"; 1311 tb_cell.innerHTML = label; 1312 } 1313 else if ( typeof code != 'function' ) 1314 { 1315 var tb_element = createButton(code); 1316 if ( tb_element ) 1317 { 1318 tb_cell = document.createElement("td"); 1319 tb_cell.className = 'toolbarElement'; 1320 tb_row.appendChild(tb_cell); 1321 tb_cell.appendChild(tb_element); 1322 } 1323 else if ( tb_element === null ) 1324 { 1325 alert("FIXME: Unknown toolbar item: " + code); 1326 } 1327 } 1328 } 1329 } 1330 1331 if ( editor.config.flowToolbars ) 1332 { 1333 toolbar.appendChild(Xinha._createToolbarBreakingElement()); 1334 } 1335 1336 return toolbar; 1337 }; 1338 1339 // @todo : is this some kind of test not finished ? 1340 // Why the hell this is not in the config object ? 1341 var use_clone_img = false; 1342 Xinha.makeBtnImg = function(imgDef, doc) 1343 { 1344 if ( !doc ) 1345 { 1346 doc = document; 1347 } 1348 1349 if ( !doc._xinhaImgCache ) 1350 { 1351 doc._xinhaImgCache = {}; 1352 Xinha.freeLater(doc._xinhaImgCache); 1353 } 1354 1355 var i_contain = null; 1356 if ( Xinha.is_ie && ( ( !doc.compatMode ) || ( doc.compatMode && doc.compatMode == "BackCompat" ) ) ) 1357 { 1358 i_contain = doc.createElement('span'); 1359 } 1360 else 1361 { 1362 i_contain = doc.createElement('div'); 1363 i_contain.style.position = 'relative'; 1364 } 1365 1366 i_contain.style.overflow = 'hidden'; 1367 i_contain.style.width = "18px"; 1368 i_contain.style.height = "18px"; 1369 i_contain.className = 'buttonImageContainer'; 1370 1371 var img = null; 1372 if ( typeof imgDef == 'string' ) 1373 { 1374 if ( doc._xinhaImgCache[imgDef] ) 1375 { 1376 img = doc._xinhaImgCache[imgDef].cloneNode(); 1377 } 1378 else 1379 { 1380 img = doc.createElement("img"); 1381 img.src = imgDef; 1382 img.style.width = "18px"; 1383 img.style.height = "18px"; 1384 if ( use_clone_img ) 1385 { 1386 doc._xinhaImgCache[imgDef] = img.cloneNode(); 1387 } 1388 } 1389 } 1390 else 1391 { 1392 if ( doc._xinhaImgCache[imgDef[0]] ) 1393 { 1394 img = doc._xinhaImgCache[imgDef[0]].cloneNode(); 1395 } 1396 else 1397 { 1398 img = doc.createElement("img"); 1399 img.src = imgDef[0]; 1400 img.style.position = 'relative'; 1401 if ( use_clone_img ) 1402 { 1403 doc._xinhaImgCache[imgDef[0]] = img.cloneNode(); 1404 } 1405 } 1406 // @todo: Using 18 dont let us use a theme with its own icon toolbar height 1407 // and width. Probably better to calculate this value 18 1408 // var sizeIcon = img.width / nb_elements_per_image; 1409 img.style.top = imgDef[2] ? ('-' + (18 * (imgDef[2] + 1)) + 'px') : '-18px'; 1410 img.style.left = imgDef[1] ? ('-' + (18 * (imgDef[1] + 1)) + 'px') : '-18px'; 1411 } 1412 i_contain.appendChild(img); 1413 return i_contain; 1414 }; 1415 1416 Xinha.prototype._createStatusBar = function() 1417 { 1418 this.setLoadingMessage('Create StatusBar'); 1419 var statusbar = document.createElement("div"); 1420 statusbar.className = "statusBar"; 1421 this._statusBar = statusbar; 1422 Xinha.freeLater(this, '_statusBar'); 1423 1424 // statusbar.appendChild(document.createTextNode(Xinha._lc("Path") + ": ")); 1425 // creates a holder for the path view 1426 var div = document.createElement("span"); 1427 div.className = "statusBarTree"; 1428 div.innerHTML = Xinha._lc("Path") + ": "; 1429 this._statusBarTree = div; 1430 Xinha.freeLater(this, '_statusBarTree'); 1431 this._statusBar.appendChild(div); 1432 1433 div = document.createElement("span"); 1434 div.innerHTML = Xinha._lc("You are in TEXT MODE. Use the [<>] button to switch back to WYSIWYG."); 1435 div.style.display = "none"; 1436 this._statusBarTextMode = div; 1437 Xinha.freeLater(this, '_statusBarTextMode'); 1438 this._statusBar.appendChild(div); 1439 1440 if ( !this.config.statusBar ) 1441 { 1442 // disable it... 1443 statusbar.style.display = "none"; 1444 } 1445 1446 return statusbar; 1447 }; 1448 1449 // Creates the Xinha object and replaces the textarea with it. 1450 Xinha.prototype.generate = function () 1451 { 1452 var i; 1453 var editor = this; // we'll need "this" in some nested functions 1454 this.setLoadingMessage('Generate Xinha object'); 1455 1456 if ( typeof Dialog == 'undefined' ) 1457 { 1458 Xinha._loadback(_editor_url + 'dialog.js', this.generate, this ); 1459 return false; 1460 } 1461 1462 if ( typeof Xinha.Dialog == 'undefined' ) 1463 { 1464 Xinha._loadback(_editor_url + 'inline-dialog.js', this.generate, this ); 1465 return false; 1466 } 1467 1468 if ( typeof PopupWin == 'undefined' ) 1469 { 1470 Xinha._loadback(_editor_url + 'popupwin.js', this.generate, this ); 1471 return false; 1472 } 1473 1474 if ( _editor_skin !== "" ) 1475 { 1476 var found = false; 1477 var head = document.getElementsByTagName("head")[0]; 1478 var links = document.getElementsByTagName("link"); 1479 for(i = 0; i<links.length; i++) 1480 { 1481 if ( ( links[i].rel == "stylesheet" ) && ( links[i].href == _editor_url + 'skins/' + _editor_skin + '/skin.css' ) ) 1482 { 1483 found = true; 1484 } 1485 } 1486 if ( !found ) 1487 { 1488 var link = document.createElement("link"); 1489 link.type = "text/css"; 1490 link.href = _editor_url + 'skins/' + _editor_skin + '/skin.css'; 1491 link.rel = "stylesheet"; 1492 head.appendChild(link); 1493 } 1494 } 1495 1496 //backwards-compatibility: load FullScreen-Plugin if we find a "popupeditor"-button in the toolbar 1497 // @todo: remove the backward compatibility in release 2.0 1498 var toolbar = editor.config.toolbar; 1499 for ( i = toolbar.length; --i >= 0; ) 1500 { 1501 for ( var j = toolbar[i].length; --j >= 0; ) 1502 { 1503 if ( toolbar[i][j]=="popupeditor" ) 1504 { 1505 if ( typeof FullScreen == "undefined" ) 1506 { 1507 // why can't we use the following line instead ? 1508 // Xinha.loadPlugin("FullScreen", this.generate ); 1509 Xinha.loadPlugin("FullScreen", function() { editor.generate(); } ); 1510 return false; 1511 } 1512 editor.registerPlugin('FullScreen'); 1513 } 1514 } 1515 } 1516 1517 // If this is gecko, set up the paragraph handling now 1518 if ( Xinha.is_gecko && editor.config.mozParaHandler == 'best' ) 1519 { 1520 if ( typeof EnterParagraphs == 'undefined' ) 1521 { 1522 // why can't we use the following line instead ? 1523 // Xinha.loadPlugin("EnterParagraphs", this.generate ); 1524 Xinha.loadPlugin("EnterParagraphs", function() { editor.generate(); } ); 1525 return false; 1526 } 1527 editor.registerPlugin('EnterParagraphs'); 1528 } 1529 1530 if ( typeof Xinha.getHTML == 'undefined' ) 1531 { 1532 Xinha._loadback(_editor_url + "getHTML.js", function() { editor.generate(); } ); 1533 return false; 1534 } 1535 1536 if ( typeof Xinha.prototype._insertImage == 'undefined' ) 1537 { 1538 Xinha._loadback(_editor_url + "popups/insert_image.js", function() { editor.generate(); } ); 1539 return false; 1540 } 1541 1542 if ( typeof Xinha.prototype._createLink == 'undefined' && typeof Linker == 'undefined' ) 1543 { 1544 Xinha._loadback(_editor_url + "popups/link.js", function() { editor.generate(); } ); 1545 return false; 1546 } 1547 1548 // create the editor framework, yah, table layout I know, but much easier 1549 // to get it working correctly this way, sorry about that, patches welcome. 1550 1551 this._framework = 1552 { 1553 'table': document.createElement('table'), 1554 'tbody': document.createElement('tbody'), // IE will not show the table if it doesn't have a tbody! 1555 'tb_row': document.createElement('tr'), 1556 'tb_cell': document.createElement('td'), // Toolbar 1557 1558 'tp_row': document.createElement('tr'), 1559 'tp_cell': this._panels.top.container, // top panel 1560 1561 'ler_row': document.createElement('tr'), 1562 'lp_cell': this._panels.left.container, // left panel 1563 'ed_cell': document.createElement('td'), // editor 1564 'rp_cell': this._panels.right.container, // right panel 1565 1566 'bp_row': document.createElement('tr'), 1567 'bp_cell': this._panels.bottom.container,// bottom panel 1568 1569 'sb_row': document.createElement('tr'), 1570 'sb_cell': document.createElement('td') // status bar 1571 1572 }; 1573 Xinha.freeLater(this._framework); 1574 1575 var fw = this._framework; 1576 fw.table.border = "0"; 1577 fw.table.cellPadding = "0"; 1578 fw.table.cellSpacing = "0"; 1579 1580 fw.tb_row.style.verticalAlign = 'top'; 1581 fw.tp_row.style.verticalAlign = 'top'; 1582 fw.ler_row.style.verticalAlign= 'top'; 1583 fw.bp_row.style.verticalAlign = 'top'; 1584 fw.sb_row.style.verticalAlign = 'top'; 1585 fw.ed_cell.style.position = 'relative'; 1586 1587 // Put the cells in the rows set col & rowspans 1588 // note that I've set all these so that all panels are showing 1589 // but they will be redone in sizeEditor() depending on which 1590 // panels are shown. It's just here to clarify how the thing 1591 // is put togethor. 1592 fw.tb_row.appendChild(fw.tb_cell); 1593 fw.tb_cell.colSpan = 3; 1594 1595 fw.tp_row.appendChild(fw.tp_cell); 1596 fw.tp_cell.colSpan = 3; 1597 1598 fw.ler_row.appendChild(fw.lp_cell); 1599 fw.ler_row.appendChild(fw.ed_cell); 1600 fw.ler_row.appendChild(fw.rp_cell); 1601 1602 fw.bp_row.appendChild(fw.bp_cell); 1603 fw.bp_cell.colSpan = 3; 1604 1605 fw.sb_row.appendChild(fw.sb_cell); 1606 fw.sb_cell.colSpan = 3; 1607 1608 // Put the rows in the table body 1609 fw.tbody.appendChild(fw.tb_row); // Toolbar 1610 fw.tbody.appendChild(fw.tp_row); // Left, Top, Right panels 1611 fw.tbody.appendChild(fw.ler_row); // Editor/Textarea 1612 fw.tbody.appendChild(fw.bp_row); // Bottom panel 1613 fw.tbody.appendChild(fw.sb_row); // Statusbar 1614 1615 // and body in the table 1616 fw.table.appendChild(fw.tbody); 1617 1618 var xinha = this._framework.table; 1619 this._htmlArea = xinha; 1620 Xinha.freeLater(this, '_htmlArea'); 1621 xinha.className = "htmlarea"; 1622 1623 // create the toolbar and put in the area 1624 this._framework.tb_cell.appendChild( this._createToolbar() ); 1625 1626 // create the IFRAME & add to container 1627 var iframe = document.createElement("iframe"); 1628 iframe.src = _editor_url + editor.config.URIs.blank; 1629 this._framework.ed_cell.appendChild(iframe); 1630 this._iframe = iframe; 1631 this._iframe.className = 'xinha_iframe'; 1632 Xinha.freeLater(this, '_iframe'); 1633 1634 // creates & appends the status bar 1635 var statusbar = this._createStatusBar(); 1636 this._framework.sb_cell.appendChild(statusbar); 1637 1638 // insert Xinha before the textarea. 1639 var textarea = this._textArea; 1640 textarea.parentNode.insertBefore(xinha, textarea); 1641 textarea.className = 'xinha_textarea'; 1642 1643 // extract the textarea and insert it into the xinha framework 1644 Xinha.removeFromParent(textarea); 1645 this._framework.ed_cell.appendChild(textarea); 1646 1647 1648 // Set up event listeners for saving the iframe content to the textarea 1649 if ( textarea.form ) 1650 { 1651 // onsubmit get the Xinha content and update original textarea. 1652 Xinha.prependDom0Event( 1653 this._textArea.form, 1654 'submit', 1655 function() 1656 { 1657 editor._textArea.value = editor.outwardHtml(editor.getHTML()); 1658 return true; 1659 } 1660 ); 1661 1662 var initialTAContent = textarea.value; 1663 1664 // onreset revert the Xinha content to the textarea content 1665 Xinha.prependDom0Event( 1666 this._textArea.form, 1667 'reset', 1668 function() 1669 { 1670 editor.setHTML(editor.inwardHtml(initialTAContent)); 1671 editor.updateToolbar(); 1672 return true; 1673 } 1674 ); 1675 1676 //add onsubmit handlers for textareas that don't have one 1677 // doesn't work in IE!! 1678 /* if ( !textarea.form.xinha_submit ) 1679 { 1680 textarea.form.xinha_submit = textarea.form.submit; 1681 textarea.form.submit = function() 1682 { 1683 for ( var i = this.elements.length; i--; ) 1684 { 1685 var element = this.elements[i]; 1686 if ( element.type != 'textarea' ) continue; 1687 for ( var a = __xinhas.length; a--; ) 1688 { 1689 var editor = __xinhas[a]; 1690 if ( editor && editor._textArea == element) 1691 { 1692 element.value = editor.outwardHtml(editor.getHTML()); 1693 } 1694 } 1695 } 1696 this.xinha_submit(); 1697 }; 1698 }*/ 1699 } 1700 1701 // add a handler for the "back/forward" case -- on body.unload we save 1702 // the HTML content into the original textarea. 1703 Xinha.prependDom0Event( 1704 window, 1705 'unload', 1706 function() 1707 { 1708 textarea.value = editor.outwardHtml(editor.getHTML()); 1709 return true; 1710 } 1711 ); 1712 1713 // Hide textarea 1714 textarea.style.display = "none"; 1715 1716 // Initalize size 1717 editor.initSize(); 1718 1719 // Add an event to initialize the iframe once loaded. 1720 editor._iframeLoadDone = false; 1721 Xinha._addEvent( 1722 this._iframe, 1723 'load', 1724 function(e) 1725 { 1726 if ( !editor._iframeLoadDone ) 1727 { 1728 editor._iframeLoadDone = true; 1729 editor.initIframe(); 1730 } 1731 return true; 1732 } 1733 ); 1734 1735 }; 1736 1737 /** 1738 * Size the editor according to the INITIAL sizing information. 1739 * config.width 1740 * The width may be set via three ways 1741 * auto = the width is inherited from the original textarea 1742 * toolbar = the width is set to be the same size as the toolbar 1743 * <set size> = the width is an explicit size (any CSS measurement, eg 100em should be fine) 1744 * 1745 * config.height 1746 * auto = the height is inherited from the original textarea 1747 * <set size> = an explicit size measurement (again, CSS measurements) 1748 * 1749 * config.sizeIncludesBars 1750 * true = the tool & status bars will appear inside the width & height confines 1751 * false = the tool & status bars will appear outside the width & height confines 1752 * 1753 */ 1754 1755 Xinha.prototype.initSize = function() 1756 { 1757 this.setLoadingMessage('Init editor size'); 1758 var editor = this; 1759 var width = null; 1760 var height = null; 1761 1762 switch ( this.config.width ) 1763 { 1764 case 'auto': 1765 width = this._initial_ta_size.w; 1766 break; 1767 1768 case 'toolbar': 1769 width = this._toolBar.offsetWidth + 'px'; 1770 break; 1771 1772 default : 1773 // @todo: check if this is better : 1774 // width = (parseInt(this.config.width, 10) == this.config.width)? this.config.width + 'px' : this.config.width; 1775 width = /[^0-9]/.test(this.config.width) ? this.config.width : this.config.width + 'px'; 1776 break; 1777 } 1778 1779 switch ( this.config.height ) 1780 { 1781 case 'auto': 1782 height = this._initial_ta_size.h; 1783 break; 1784 1785 default : 1786 // @todo: check if this is better : 1787 // height = (parseInt(this.config.height, 10) == this.config.height)? this.config.height + 'px' : this.config.height; 1788 height = /[^0-9]/.test(this.config.height) ? this.config.height : this.config.height + 'px'; 1789 break; 1790 } 1791 1792 this.sizeEditor(width, height, this.config.sizeIncludesBars, this.config.sizeIncludesPanels); 1793 1794 // why can't we use the following line instead ? 1795 // this.notifyOn('panel_change',this.sizeEditor); 1796 this.notifyOn('panel_change',function() { editor.sizeEditor(); }); 1797 }; 1798 1799 /** 1800 * Size the editor to a specific size, or just refresh the size (when window resizes for example) 1801 * @param width optional width (CSS specification) 1802 * @param height optional height (CSS specification) 1803 * @param includingBars optional boolean to indicate if the size should include or exclude tool & status bars 1804 */ 1805 Xinha.prototype.sizeEditor = function(width, height, includingBars, includingPanels) 1806 { 1807 1808 // We need to set the iframe & textarea to 100% height so that the htmlarea 1809 // isn't "pushed out" when we get it's height, so we can change them later. 1810 this._iframe.style.height = '100%'; 1811 this._textArea.style.height = '100%'; 1812 this._iframe.style.width = ''; 1813 this._textArea.style.width = ''; 1814 1815 if ( includingBars !== null ) 1816 { 1817 this._htmlArea.sizeIncludesToolbars = includingBars; 1818 } 1819 if ( includingPanels !== null ) 1820 { 1821 this._htmlArea.sizeIncludesPanels = includingPanels; 1822 } 1823 1824 if ( width ) 1825 { 1826 this._htmlArea.style.width = width; 1827 if ( !this._htmlArea.sizeIncludesPanels ) 1828 { 1829 // Need to add some for l & r panels 1830 var rPanel = this._panels.right; 1831 if ( rPanel.on && rPanel.panels.length && Xinha.hasDisplayedChildren(rPanel.div) ) 1832 { 1833 this._htmlArea.style.width = (this._htmlArea.offsetWidth + parseInt(this.config.panel_dimensions.right, 10)) + 'px'; 1834 } 1835 1836 var lPanel = this._panels.left; 1837 if ( lPanel.on && lPanel.panels.length && Xinha.hasDisplayedChildren(lPanel.div) ) 1838 { 1839 this._htmlArea.style.width = (this._htmlArea.offsetWidth + parseInt(this.config.panel_dimensions.left, 10)) + 'px'; 1840 } 1841 } 1842 } 1843 1844 if ( height ) 1845 { 1846 this._htmlArea.style.height = height; 1847 if ( !this._htmlArea.sizeIncludesToolbars ) 1848 { 1849 // Need to add some for toolbars 1850 this._htmlArea.style.height = (this._htmlArea.offsetHeight + this._toolbar.offsetHeight + this._statusBar.offsetHeight) + 'px'; 1851 } 1852 1853 if ( !this._htmlArea.sizeIncludesPanels ) 1854 { 1855 // Need to add some for t & b panels 1856 var tPanel = this._panels.top; 1857 if ( tPanel.on && tPanel.panels.length && Xinha.hasDisplayedChildren(tPanel.div) ) 1858 { 1859 this._htmlArea.style.height = (this._htmlArea.offsetHeight + parseInt(this.config.panel_dimensions.top, 10)) + 'px'; 1860 } 1861 1862 var bPanel = this._panels.bottom; 1863 if ( bPanel.on && bPanel.panels.length && Xinha.hasDisplayedChildren(bPanel.div) ) 1864 { 1865 this._htmlArea.style.height = (this._htmlArea.offsetHeight + parseInt(this.config.panel_dimensions.bottom, 10)) + 'px'; 1866 } 1867 } 1868 } 1869 1870 // At this point we have this._htmlArea.style.width & this._htmlArea.style.height 1871 // which are the size for the OUTER editor area, including toolbars and panels 1872 // now we size the INNER area and position stuff in the right places. 1873 width = this._htmlArea.offsetWidth; 1874 height = this._htmlArea.offsetHeight; 1875 1876 // Set colspan for toolbar, and statusbar, rowspan for left & right panels, and insert panels to be displayed 1877 // into thier rows 1878 var panels = this._panels; 1879 var editor = this; 1880 var col_span = 1; 1881 1882 function panel_is_alive(pan) 1883 { 1884 if ( panels[pan].on && panels[pan].panels.length && Xinha.hasDisplayedChildren(panels[pan].container) ) 1885 { 1886 panels[pan].container.style.display = ''; 1887 return true; 1888 } 1889 // Otherwise make sure it's been removed from the framework 1890 else 1891 { 1892 panels[pan].container.style.display='none'; 1893 return false; 1894 } 1895 } 1896 1897 if ( panel_is_alive('left') ) 1898 { 1899 col_span += 1; 1900 } 1901 1902 // if ( panel_is_alive('top') ) 1903 // { 1904 // NOP 1905 // } 1906 1907 if ( panel_is_alive('right') ) 1908 { 1909 col_span += 1; 1910 } 1911 1912 // if ( panel_is_alive('bottom') ) 1913 // { 1914 // NOP 1915 // } 1916 1917 this._framework.tb_cell.colSpan = col_span; 1918 this._framework.tp_cell.colSpan = col_span; 1919 this._framework.bp_cell.colSpan = col_span; 1920 this._framework.sb_cell.colSpan = col_span; 1921 1922 // Put in the panel rows, top panel goes above editor row 1923 if ( !this._framework.tp_row.childNodes.length ) 1924 { 1925 Xinha.removeFromParent(this._framework.tp_row); 1926 } 1927 else 1928 { 1929 if ( !Xinha.hasParentNode(this._framework.tp_row) ) 1930 { 1931 this._framework.tbody.insertBefore(this._framework.tp_row, this._framework.ler_row); 1932 } 1933 } 1934 1935 // bp goes after the editor 1936 if ( !this._framework.bp_row.childNodes.length ) 1937 { 1938 Xinha.removeFromParent(this._framework.bp_row); 1939 } 1940 else 1941 { 1942 if ( !Xinha.hasParentNode(this._framework.bp_row) ) 1943 { 1944 this._framework.tbody.insertBefore(this._framework.bp_row, this._framework.ler_row.nextSibling); 1945 } 1946 } 1947 1948 // finally if the statusbar is on, insert it 1949 if ( !this.config.statusBar ) 1950 { 1951 Xinha.removeFromParent(this._framework.sb_row); 1952 } 1953 else 1954 { 1955 if ( !Xinha.hasParentNode(this._framework.sb_row) ) 1956 { 1957 this._framework.table.appendChild(this._framework.sb_row); 1958 } 1959 } 1960 1961 // Size and set colspans, link up the framework 1962 this._framework.lp_cell.style.width = this.config.panel_dimensions.left; 1963 this._framework.rp_cell.style.width = this.config.panel_dimensions.right; 1964 this._framework.tp_cell.style.height = this.config.panel_dimensions.top; 1965 this._framework.bp_cell.style.height = this.config.panel_dimensions.bottom; 1966 this._framework.tb_cell.style.height = this._toolBar.offsetHeight + 'px'; 1967 this._framework.sb_cell.style.height = this._statusBar.offsetHeight + 'px'; 1968 1969 var edcellheight = height - this._toolBar.offsetHeight - this._statusBar.offsetHeight; 1970 if ( panel_is_alive('top') ) 1971 { 1972 edcellheight -= parseInt(this.config.panel_dimensions.top, 10); 1973 } 1974 if ( panel_is_alive('bottom') ) 1975 { 1976 edcellheight -= parseInt(this.config.panel_dimensions.bottom, 10); 1977 } 1978 this._iframe.style.height = edcellheight + 'px'; 1979 // this._framework.rp_cell.style.height = edcellheight + 'px'; 1980 // this._framework.lp_cell.style.height = edcellheight + 'px'; 1981 1982 // (re)size the left and right panels so they are equal the editor height 1983 // for(var i = 0; i < this._panels.left.panels.length; i++) 1984 // { 1985 // this._panels.left.panels[i].style.height = this._iframe.style.height; 1986 // } 1987 1988 // for(var i = 0; i < this._panels.right.panels.length; i++) 1989 // { 1990 // this._panels.right.panels[i].style.height = this._iframe.style.height; 1991 // } 1992 1993 var edcellwidth = width; 1994 if ( panel_is_alive('left') ) 1995 { 1996 edcellwidth -= parseInt(this.config.panel_dimensions.left, 10); 1997 } 1998 if ( panel_is_alive('right') ) 1999 { 2000 edcellwidth -= parseInt(this.config.panel_dimensions.right, 10); 2001 } 2002 this._iframe.style.width = edcellwidth + 'px'; 2003 2004 this._textArea.style.height = this._iframe.style.height; 2005 this._textArea.style.width = this._iframe.style.width; 2006 2007 this.notifyOf('resize', {width:this._htmlArea.offsetWidth, height:this._htmlArea.offsetHeight}); 2008 }; 2009 2010 Xinha.prototype.addPanel = function(side) 2011 { 2012 var div = document.createElement('div'); 2013 div.side = side; 2014 if ( side == 'left' || side == 'right' ) 2015 { 2016 div.style.width = this.config.panel_dimensions[side]; 2017 if(this._iframe) div.style.height = this._iframe.style.height; 2018 } 2019 Xinha.addClasses(div, 'panel'); 2020 this._panels[side].panels.push(div); 2021 this._panels[side].div.appendChild(div); 2022 2023 this.notifyOf('panel_change', {'action':'add','panel':div}); 2024 2025 return div; 2026 }; 2027 2028 2029 Xinha.prototype.removePanel = function(panel) 2030 { 2031 this._panels[panel.side].div.removeChild(panel); 2032 var clean = []; 2033 for ( var i = 0; i < this._panels[panel.side].panels.length; i++ ) 2034 { 2035 if ( this._panels[panel.side].panels[i] != panel ) 2036 { 2037 clean.push(this._panels[panel.side].panels[i]); 2038 } 2039 } 2040 this._panels[panel.side].panels = clean; 2041 this.notifyOf('panel_change', {'action':'remove','panel':panel}); 2042 }; 2043 2044 Xinha.prototype.hidePanel = function(panel) 2045 { 2046 if ( panel && panel.style.display != 'none' ) 2047 { 2048 panel.style.display = 'none'; 2049 this.notifyOf('panel_change', {'action':'hide','panel':panel}); 2050 } 2051 }; 2052 2053 Xinha.prototype.showPanel = function(panel) 2054 { 2055 if ( panel && panel.style.display == 'none' ) 2056 { 2057 panel.style.display = ''; 2058 this.notifyOf('panel_change', {'action':'show','panel':panel}); 2059 } 2060 }; 2061 2062 Xinha.prototype.hidePanels = function(sides) 2063 { 2064 if ( typeof sides == 'undefined' ) 2065 { 2066 sides = ['left','right','top','bottom']; 2067 } 2068 2069 var reShow = []; 2070 for ( var i = 0; i < sides.length;i++ ) 2071 { 2072 if ( this._panels[sides[i]].on ) 2073 { 2074 reShow.push(sides[i]); 2075 this._panels[sides[i]].on = false; 2076 } 2077 } 2078 this.notifyOf('panel_change', {'action':'multi_hide','sides':sides}); 2079 }; 2080 2081 Xinha.prototype.showPanels = function(sides) 2082 { 2083 if ( typeof sides == 'undefined' ) 2084 { 2085 sides = ['left','right','top','bottom']; 2086 } 2087 2088 var reHide = []; 2089 for ( var i = 0; i < sides.length; i++ ) 2090 { 2091 if ( !this._panels[sides[i]].on ) 2092 { 2093 reHide.push(sides[i]); 2094 this._panels[sides[i]].on = true; 2095 } 2096 } 2097 this.notifyOf('panel_change', {'action':'multi_show','sides':sides}); 2098 }; 2099 2100 Xinha.objectProperties = function(obj) 2101 { 2102 var props = []; 2103 for ( var x in obj ) 2104 { 2105 props[props.length] = x; 2106 } 2107 return props; 2108 }; 2109 2110 /* 2111 * EDITOR ACTIVATION NOTES: 2112 * when a page has multiple Xinha editors, ONLY ONE should be activated at any time (this is mostly to 2113 * work around a bug in Mozilla, but also makes some sense). No editor should be activated or focused 2114 * automatically until at least one editor has been activated through user action (by mouse-clicking in 2115 * the editor). 2116 */ 2117 Xinha.prototype.editorIsActivated = function() 2118 { 2119 try 2120 { 2121 return Xinha.is_gecko? this._doc.designMode == 'on' : this._doc.body.contentEditable; 2122 } 2123 catch (ex) 2124 { 2125 return false; 2126 } 2127 }; 2128 2129 Xinha._someEditorHasBeenActivated = false; 2130 Xinha._currentlyActiveEditor = false; 2131 Xinha.prototype.activateEditor = function() 2132 { 2133 // We only want ONE editor at a time to be active 2134 if ( Xinha._currentlyActiveEditor ) 2135 { 2136 if ( Xinha._currentlyActiveEditor == this ) 2137 { 2138 return true; 2139 } 2140 Xinha._currentlyActiveEditor.deactivateEditor(); 2141 } 2142 2143 if ( Xinha.is_gecko && this._doc.designMode != 'on' ) 2144 { 2145 try 2146 { 2147 // cannot set design mode if no display 2148 if ( this._iframe.style.display == 'none' ) 2149 { 2150 this._iframe.style.display = ''; 2151 this._doc.designMode = 'on'; 2152 this._iframe.style.display = 'none'; 2153 } 2154 else 2155 { 2156 this._doc.designMode = 'on'; 2157 } 2158 } catch (ex) {} 2159 } 2160 else if ( !Xinha.is_gecko && this._doc.body.contentEditable !== true ) 2161 { 2162 this._doc.body.contentEditable = true; 2163 } 2164 2165 // We need to know that at least one editor on the page has been activated 2166 // this is because we will not focus any editor until an editor has been activated 2167 Xinha._someEditorHasBeenActivated = true; 2168 Xinha._currentlyActiveEditor = this; 2169 2170 var editor = this; 2171 this.enableToolbar(); 2172 }; 2173 2174 Xinha.prototype.deactivateEditor = function() 2175 { 2176 // If the editor isn't active then the user shouldn't use the toolbar 2177 this.disableToolbar(); 2178 2179 if ( Xinha.is_gecko && this._doc.designMode != 'off' ) 2180 { 2181 try 2182 { 2183 this._doc.designMode = 'off'; 2184 } catch (ex) {} 2185 } 2186 else if ( !Xinha.is_gecko && this._doc.body.contentEditable !== false ) 2187 { 2188 this._doc.body.contentEditable = false; 2189 } 2190 2191 if ( Xinha._currentlyActiveEditor != this ) 2192 { 2193 // We just deactivated an editor that wasn't marked as the currentlyActiveEditor 2194 2195 return; // I think this should really be an error, there shouldn't be a situation where 2196 // an editor is deactivated without first being activated. but it probably won't 2197 // hurt anything. 2198 } 2199 2200 Xinha._currentlyActiveEditor = false; 2201 }; 2202 2203 Xinha.prototype.initIframe = function() 2204 { 2205 this.setLoadingMessage('Init IFrame'); 2206 this.disableToolbar(); 2207 var doc = null; 2208 var editor = this; 2209 try 2210 { 2211 if ( editor._iframe.contentDocument ) 2212 { 2213 this._doc = editor._iframe.contentDocument; 2214 } 2215 else 2216 { 2217 this._doc = editor._iframe.contentWindow.document; 2218 } 2219 doc = this._doc; 2220 // try later 2221 if ( !doc ) 2222 { 2223 if ( Xinha.is_gecko ) 2224 { 2225 setTimeout(function() { editor.initIframe(); }, 50); 2226 return false; 2227 } 2228 else 2229 { 2230 alert("ERROR: IFRAME can't be initialized."); 2231 } 2232 } 2233 } 2234 catch(ex) 2235 { // try later 2236 setTimeout(function() { editor.initIframe(); }, 50); 2237 } 2238 2239 Xinha.freeLater(this, '_doc'); 2240 2241 doc.open("text/html","replace"); 2242 var html = ''; 2243 if ( !editor.config.fullPage ) 2244 { 2245 html = "<html>\n"; 2246 html += "<head>\n"; 2247 html += "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + editor.config.charSet + "\">\n"; 2248 if ( typeof editor.config.baseHref != 'undefined' && editor.config.baseHref !== null ) 2249 { 2250 html += "<base href=\"" + editor.config.baseHref + "\"/>\n"; 2251 } 2252 html += "<style title=\"table borders\">"; 2253 html += ".htmtableborders, .htmtableborders td, .htmtableborders th {border : 1px dashed lightgrey ! important;} \n"; 2254 html += "</style>\n"; 2255 html += "<style type=\"text/css\">"; 2256 html += "html, body { border: 0px; } \n"; 2257 html += "body { background-color: #ffffff; } \n"; 2258 html += "span.macro, span.macro ul, span.macro div, span.macro p {background : #CCCCCC;}\n"; 2259 html += "</style>\n"; 2260 2261 if ( editor.config.pageStyle ) 2262 { 2263 html += "<style type=\"text/css\">\n" + editor.config.pageStyle + "\n</style>"; 2264 } 2265 2266 if ( typeof editor.config.pageStyleSheets !== 'undefined' ) 2267 { 2268 for ( var i = 0; i < editor.config.pageStyleSheets.length; i++ ) 2269 { 2270 if ( editor.config.pageStyleSheets[i].length > 0 ) 2271 { 2272 html += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + editor.config.pageStyleSheets[i] + "\">"; 2273 //html += "<style> @import url('" + editor.config.pageStyleSheets[i] + "'); </style>\n"; 2274 } 2275 } 2276 } 2277 html += "</head>\n"; 2278 html += "<body>\n"; 2279 html += editor.inwardHtml(editor._textArea.value); 2280 html += "</body>\n"; 2281 html += "</html>"; 2282 } 2283 else 2284 { 2285 html = editor.inwardHtml(editor._textArea.value); 2286 if ( html.match(Xinha.RE_doctype) ) 2287 { 2288 editor.setDoctype(RegExp.$1); 2289 html = html.replace(Xinha.RE_doctype, ""); 2290 } 2291 } 2292 doc.write(html); 2293 doc.close(); 2294 2295 this.setEditorEvents(); 2296 }; 2297 2298 /** 2299 * Delay a function until the document is ready for operations. 2300 * See ticket:547 2301 * @param {object} F (Function) The function to call once the document is ready 2302 * @public 2303 */ 2304 Xinha.prototype.whenDocReady = function(F) 2305 { 2306 var E = this; 2307 if ( this._doc && this._doc.body ) 2308 { 2309 F(); 2310 } 2311 else 2312 { 2313 setTimeout(function() { E.whenDocReady(F); }, 50); 2314 } 2315 }; 2316 2317 // Switches editor mode; parameter can be "textmode" or "wysiwyg". If no 2318 // parameter was passed this function toggles between modes. 2319 Xinha.prototype.setMode = function(mode) 2320 { 2321 var html; 2322 if ( typeof mode == "undefined" ) 2323 { 2324 mode = this._editMode == "textmode" ? "wysiwyg" : "textmode"; 2325 } 2326 switch ( mode ) 2327 { 2328 case "textmode": 2329 html = this.outwardHtml(this.getHTML()); 2330 this.setHTML(html); 2331 2332 // Hide the iframe 2333 this.deactivateEditor(); 2334 this._iframe.style.display = 'none'; 2335 this._textArea.style.display = ''; 2336 2337 if ( this.config.statusBar ) 2338 { 2339 this._statusBarTree.style.display = "none"; 2340 this._statusBarTextMode.style.display = ""; 2341 } 2342 2343 this.notifyOf('modechange', {'mode':'text'}); 2344 break; 2345 2346 case "wysiwyg": 2347 html = this.inwardHtml(this.getHTML()); 2348 this.deactivateEditor(); 2349 this.setHTML(html); 2350 this._iframe.style.display = ''; 2351 this._textArea.style.display = "none"; 2352 this.activateEditor(); 2353 if ( this.config.statusBar ) 2354 { 2355 this._statusBarTree.style.display = ""; 2356 this._statusBarTextMode.style.display = "none"; 2357 } 2358 2359 this.notifyOf('modechange', {'mode':'wysiwyg'}); 2360 break; 2361 2362 default: 2363 alert("Mode <" + mode + "> not defined!"); 2364 return false; 2365 } 2366 this._editMode = mode; 2367 2368 for ( var i in this.plugins ) 2369 { 2370 var plugin = this.plugins[i].instance; 2371 if ( plugin && typeof plugin.onMode == "function" ) 2372 { 2373 plugin.onMode(mode); 2374 } 2375 } 2376 }; 2377 2378 Xinha.prototype.setFullHTML = function(html) 2379 { 2380 var save_multiline = RegExp.multiline; 2381 RegExp.multiline = true; 2382 if ( html.match(Xinha.RE_doctype) ) 2383 { 2384 this.setDoctype(RegExp.$1); 2385 html = html.replace(Xinha.RE_doctype, ""); 2386 } 2387 RegExp.multiline = save_multiline; 2388 if ( !Xinha.is_ie ) 2389 { 2390 if ( html.match(Xinha.RE_head) ) 2391 { 2392 this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1; 2393 } 2394 if ( html.match(Xinha.RE_body) ) 2395 { 2396 this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1; 2397 } 2398 } 2399 else 2400 { 2401 var reac = this.editorIsActivated(); 2402 if ( reac ) 2403 { 2404 this.deactivateEditor(); 2405 } 2406 var html_re = /<html>((.|\n)*?)<\/html>/i; 2407 html = html.replace(html_re, "$1"); 2408 this._doc.open("text/html","replace"); 2409 this._doc.write(html); 2410 this._doc.close(); 2411 if ( reac ) 2412 { 2413 this.activateEditor(); 2414 } 2415 this.setEditorEvents(); 2416 return true; 2417 } 2418 }; 2419 2420 Xinha.prototype.setEditorEvents = function() 2421 { 2422 var editor=this; 2423 var doc=this._doc; 2424 editor.whenDocReady( 2425 function() 2426 { 2427 // if we have multiple editors some bug in Mozilla makes some lose editing ability 2428 Xinha._addEvents( 2429 doc, 2430 ["mousedown"], 2431 function() 2432 { 2433 editor.activateEditor(); 2434 return true; 2435 } 2436 ); 2437 2438 // intercept some events; for updating the toolbar & keyboard handlers 2439 Xinha._addEvents( 2440 doc, 2441 ["keydown", "keypress", "mousedown", "mouseup", "drag"], 2442 function (event) 2443 { 2444 return editor._editorEvent(Xinha.is_ie ? editor._iframe.contentWindow.event : event); 2445 } 2446 ); 2447 2448 // check if any plugins have registered refresh handlers 2449 for ( var i in editor.plugins ) 2450 { 2451 var plugin = editor.plugins[i].instance; 2452 Xinha.refreshPlugin(plugin); 2453 } 2454 2455 // specific editor initialization 2456 if ( typeof editor._onGenerate == "function" ) 2457 { 2458 editor._onGenerate(); 2459 } 2460 2461 Xinha.addDom0Event(window, 'resize', function(e) { editor.sizeEditor(); }); 2462 editor.removeLoadingMessage(); 2463 } 2464 ); 2465 }; 2466 2467 /*************************************************** 2468 * Category: PLUGINS 2469 ***************************************************/ 2470 2471 // Create the specified plugin and register it with this Xinha 2472 // return the plugin created to allow refresh when necessary 2473 Xinha.prototype.registerPlugin = function() 2474 { 2475 var plugin = arguments[0]; 2476 2477 // @todo : try to avoid the use of eval() 2478 // We can only register plugins that have been succesfully loaded 2479 if ( plugin === null || typeof plugin == 'undefined' || (typeof plugin == 'string' && eval('typeof ' + plugin) == 'undefined') ) 2480 { 2481 return false; 2482 } 2483 2484 var args = []; 2485 for ( var i = 1; i < arguments.length; ++i ) 2486 { 2487 args.push(arguments[i]); 2488 } 2489 return this.registerPlugin2(plugin, args); 2490 }; 2491 2492 // this is the variant of the function above where the plugin arguments are 2493 // already packed in an array. Externally, it should be only used in the 2494 // full-screen editor code, in order to initialize plugins with the same 2495 // parameters as in the opener window. 2496 Xinha.prototype.registerPlugin2 = function(plugin, args) 2497 { 2498 // @todo : try to avoid the use of eval() 2499 if ( typeof plugin == "string" ) 2500 { 2501 plugin = eval(plugin); 2502 } 2503 if ( typeof plugin == "undefined" ) 2504 { 2505 /* FIXME: This should never happen. But why does it do? */ 2506 return false; 2507 } 2508 var obj = new plugin(this, args); 2509 if ( obj ) 2510 { 2511 var clone = {}; 2512 var info = plugin._pluginInfo; 2513 for ( var i in info ) 2514 { 2515 clone[i] = info[i]; 2516 } 2517 clone.instance = obj; 2518 clone.args = args; 2519 this.plugins[plugin._pluginInfo.name] = clone; 2520 return obj; 2521 } 2522 else 2523 { 2524 alert("Can't register plugin " + plugin.toString() + "."); 2525 } 2526 }; 2527 2528 // static function that loads the required plugin and lang file, based on the 2529 // language loaded already for Xinha. You better make sure that the plugin 2530 // _has_ that language, otherwise shit might happen ;-) 2531 Xinha.getPluginDir = function(pluginName) 2532 { 2533 return _editor_url + "plugins/" + pluginName; 2534 }; 2535 2536 Xinha.loadPlugin = function(pluginName, callback) 2537 { 2538 // @todo : try to avoid the use of eval() 2539 // Might already be loaded 2540 if ( eval('typeof ' + pluginName) != 'undefined' ) 2541 { 2542 if ( callback ) 2543 { 2544 callback(pluginName); 2545 } 2546 return true; 2547 } 2548 2549 var dir = this.getPluginDir(pluginName); 2550 var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g, function (str, l1, l2, l3) { return l1 + "-" + l2.toLowerCase() + l3; }).toLowerCase() + ".js"; 2551 var plugin_file = dir + "/" + plugin; 2552 2553 Xinha._loadback(plugin_file, callback ? function() { callback(pluginName); } : null); 2554 return false; 2555 }; 2556 2557 Xinha._pluginLoadStatus = {}; 2558 Xinha._browserSpecificFunctionsLoaded = false; 2559 Xinha.loadPlugins = function(plugins, callbackIfNotReady) 2560 { 2561 // Rip the ones that are loaded and look for ones that have failed 2562 var retVal = true; 2563 var nuPlugins = Xinha.cloneObject(plugins); 2564 2565 if ( !Xinha._browserSpecificFunctionsLoaded ) 2566 { 2567 if (Xinha.is_ie) 2568 { 2569 Xinha._loadback(_editor_url + "functionsIE.js",callbackIfNotReady); 2570 } 2571 else 2572 { 2573 Xinha._loadback(_editor_url + "functionsMozilla.js",callbackIfNotReady); 2574 } 2575 return false; 2576 } 2577 while ( nuPlugins.length ) 2578 { 2579 var p = nuPlugins.pop(); 2580 if ( typeof Xinha._pluginLoadStatus[p] == 'undefined' ) 2581 { 2582 // Load it 2583 Xinha._pluginLoadStatus[p] = 'loading'; 2584 Xinha.loadPlugin(p, 2585 function(plugin) 2586 { 2587 // @todo : try to avoid the use of eval() 2588 if ( eval('typeof ' + plugin) != 'undefined' ) 2589 { 2590 Xinha._pluginLoadStatus[plugin] = 'ready'; 2591 } 2592 else 2593 { 2594 // Actually, this won't happen, because if the script fails 2595 // it will throw an exception preventing the callback from 2596 // running. This will leave it always in the "loading" state 2597 // unfortunatly that means we can't fail plugins gracefully 2598 // by just skipping them. 2599 Xinha._pluginLoadStatus[plugin] = 'failed'; 2600 } 2601 } 2602 ); 2603 retVal = false; 2604 } 2605 else 2606 { 2607 // @todo: a simple (if) would not be better than this tortuous (switch) structure ? 2608 // if ( Xinha._pluginLoadStatus[p] !== 'failed' && Xinha._pluginLoadStatus[p] !== 'ready' ) 2609 // { 2610 // retVal = false; 2611 // } 2612 switch ( Xinha._pluginLoadStatus[p] ) 2613 { 2614 case 'failed': 2615 case 'ready' : 2616 break; 2617 2618 //case 'loading': 2619 default : 2620 retVal = false; 2621 break; 2622 } 2623 } 2624 } 2625 2626 // All done, just return 2627 if ( retVal ) 2628 { 2629 return true; 2630 } 2631 2632 // Waiting on plugins to load, return false now and come back a bit later 2633 // if we have to callback 2634 if ( callbackIfNotReady ) 2635 { 2636 setTimeout(function() { if ( Xinha.loadPlugins(plugins, callbackIfNotReady) ) { callbackIfNotReady(); } }, 150); 2637 } 2638 return retVal; 2639 }; 2640 2641 // refresh plugin by calling onGenerate or onGenerateOnce method. 2642 Xinha.refreshPlugin = function(plugin) 2643 { 2644 if ( plugin && typeof plugin.onGenerate == "function" ) 2645 { 2646 plugin.onGenerate(); 2647 } 2648 if ( plugin && typeof plugin.onGenerateOnce == "function" ) 2649 { 2650 plugin.onGenerateOnce(); 2651 plugin.onGenerateOnce = null; 2652 } 2653 }; 2654 2655 Xinha.loadStyle = function(style, plugin) 2656 { 2657 var url = _editor_url || ''; 2658 if ( typeof plugin != "undefined" ) 2659 { 2660 url += "plugins/" + plugin + "/"; 2661 } 2662 url += style; 2663 // @todo: would not it be better to check the first character instead of a regex ? 2664 // if ( typeof style == 'string' && style.charAt(0) == '/' ) 2665 // { 2666 // url = style; 2667 // } 2668 if ( /^\//.test(style) ) 2669 { 2670 url = style; 2671 } 2672 var head = document.getElementsByTagName("head")[0]; 2673 var link = document.createElement("link"); 2674 link.rel = "stylesheet"; 2675 link.href = url; 2676 head.appendChild(link); 2677 //document.write("<style type='text/css'>@import url(" + url + ");</style>"); 2678 }; 2679 Xinha.loadStyle(typeof _editor_css == "string" ? _editor_css : "Xinha.css"); 2680 2681 /*************************************************** 2682 * Category: EDITOR UTILITIES 2683 ***************************************************/ 2684 2685 Xinha.prototype.debugTree = function() 2686 { 2687 var ta = document.createElement("textarea"); 2688 ta.style.width = "100%"; 2689 ta.style.height = "20em"; 2690 ta.value = ""; 2691 function debug(indent, str) 2692 { 2693 for ( ; --indent >= 0; ) 2694 { 2695 ta.value += " "; 2696 } 2697 ta.value += str + "\n"; 2698 } 2699 function _dt(root, level) 2700 { 2701 var tag = root.tagName.toLowerCase(), i; 2702 var ns = Xinha.is_ie ? root.scopeName : root.prefix; 2703 debug(level, "- " + tag + " [" + ns + "]"); 2704 for ( i = root.firstChild; i; i = i.nextSibling ) 2705 { 2706 if ( i.nodeType == 1 ) 2707 { 2708 _dt(i, level + 2); 2709 } 2710 } 2711 } 2712 _dt(this._doc.body, 0); 2713 document.body.appendChild(ta); 2714 }; 2715 2716 Xinha.getInnerText = function(el) 2717 { 2718 var txt = '', i; 2719 for ( i = el.firstChild; i; i = i.nextSibling ) 2720 { 2721 if ( i.nodeType == 3 ) 2722 { 2723 txt += i.data; 2724 } 2725 else if ( i.nodeType == 1 ) 2726 { 2727 txt += Xinha.getInnerText(i); 2728 } 2729 } 2730 return txt; 2731 }; 2732 2733 Xinha.prototype._wordClean = function() 2734 { 2735 var editor = this; 2736 var stats = 2737 { 2738 empty_tags : 0, 2739 mso_class : 0, 2740 mso_style : 0, 2741 mso_xmlel : 0, 2742 orig_len : this._doc.body.innerHTML.length, 2743 T : (new Date()).getTime() 2744 }; 2745 var stats_txt = 2746 { 2747 empty_tags : "Empty tags removed: ", 2748 mso_class : "MSO class names removed: ", 2749 mso_style : "MSO inline style removed: ", 2750 mso_xmlel : "MSO XML elements stripped: " 2751 }; 2752 2753 function showStats() 2754 { 2755 var txt = "Xinha word cleaner stats: \n\n"; 2756 for ( var i in stats ) 2757 { 2758 if ( stats_txt[i] ) 2759 { 2760 txt += stats_txt[i] + stats[i] + "\n"; 2761 } 2762 } 2763 txt += "\nInitial document length: " + stats.orig_len + "\n"; 2764 txt += "Final document length: " + editor._doc.body.innerHTML.length + "\n"; 2765 txt += "Clean-up took " + (((new Date()).getTime() - stats.T) / 1000) + " seconds"; 2766 alert(txt); 2767 } 2768 2769 function clearClass(node) 2770 { 2771 var newc = node.className.replace(/(^|\s)mso.*?(\s|$)/ig, ' '); 2772 if ( newc != node.className ) 2773 { 2774 node.className = newc; 2775 if ( ! ( /\S/.test(node.className) ) ) 2776 { 2777 node.removeAttribute("className"); 2778 ++stats.mso_class; 2779 } 2780 } 2781 } 2782 2783 function clearStyle(node) 2784 { 2785 var declarations = node.style.cssText.split(/\s*;\s*/); 2786 for ( var i = declarations.length; --i >= 0; ) 2787 { 2788 if ( ( /^mso|^tab-stops/i.test(declarations[i]) ) || ( /^margin\s*:\s*0..\s+0..\s+0../i.test(declarations[i]) ) ) 2789 { 2790 ++stats.mso_style; 2791 declarations.splice(i, 1); 2792 } 2793 } 2794 node.style.cssText = declarations.join("; "); 2795 } 2796 2797 var stripTag = null; 2798 if ( Xinha.is_ie ) 2799 { 2800 stripTag = function(el) 2801 { 2802 el.outerHTML = Xinha.htmlEncode(el.innerText); 2803 ++stats.mso_xmlel; 2804 }; 2805 } 2806 else 2807 { 2808 stripTag = function(el) 2809 { 2810 var txt = document.createTextNode(Xinha.getInnerText(el)); 2811 el.parentNode.insertBefore(txt, el); 2812 Xinha.removeFromParent(el); 2813 ++stats.mso_xmlel; 2814 }; 2815 } 2816 2817 function checkEmpty(el) 2818 { 2819 // @todo : check if this is quicker 2820 // if (!['A','SPAN','B','STRONG','I','EM','FONT'].contains(el.tagName) && !el.firstChild) 2821 if ( /^(a|span|b|strong|i|em|font)$/i.test(el.tagName) && !el.firstChild) 2822 { 2823 Xinha.removeFromParent(el); 2824 ++stats.empty_tags; 2825 } 2826 } 2827 2828 function parseTree(root) 2829 { 2830 var tag = root.tagName.toLowerCase(), i, next; 2831 // @todo : probably better to use String.indexOf() instead of this ugly regex 2832 // if ((Xinha.is_ie && root.scopeName != 'HTML') || (!Xinha.is_ie && tag.indexOf(':') !== -1)) { 2833 if ( ( Xinha.is_ie && root.scopeName != 'HTML' ) || ( !Xinha.is_ie && ( /:/.test(tag) ) ) ) 2834 { 2835 stripTag(root); 2836 return false; 2837 } 2838 else 2839 { 2840 clearClass(root); 2841 clearStyle(root); 2842 for ( i = root.firstChild; i; i = next ) 2843 { 2844 next = i.nextSibling; 2845 if ( i.nodeType == 1 && parseTree(i) ) 2846 { 2847 checkEmpty(i); 2848 } 2849 } 2850 } 2851 return true; 2852 } 2853 parseTree(this._doc.body); 2854 // showStats(); 2855 // this.debugTree(); 2856 // this.setHTML(this.getHTML()); 2857 // this.setHTML(this.getInnerHTML()); 2858 // this.forceRedraw(); 2859 this.updateToolbar(); 2860 }; 2861 2862 Xinha.prototype._clearFonts = function() 2863 { 2864 var D = this.getInnerHTML(); 2865 2866 if ( confirm(Xinha._lc("Would you like to clear font typefaces?")) ) 2867 { 2868 D = D.replace(/face="[^"]*"/gi, ''); 2869 D = D.replace(/font-family:[^;}"']+;?/gi, ''); 2870 } 2871 2872 if ( confirm(Xinha._lc("Would you like to clear font sizes?")) ) 2873 { 2874 D = D.replace(/size="[^"]*"/gi, ''); 2875 D = D.replace(/font-size:[^;}"']+;?/gi, ''); 2876 } 2877 2878 if ( confirm(Xinha._lc("Would you like to clear font colours?")) ) 2879 { 2880 D = D.replace(/color="[^"]*"/gi, ''); 2881 D = D.replace(/([^-])color:[^;}"']+;?/gi, '$1'); 2882 } 2883 2884 D = D.replace(/(style|class)="\s*"/gi, ''); 2885 D = D.replace(/<(font|span)\s*>/gi, ''); 2886 this.setHTML(D); 2887 this.updateToolbar(); 2888 }; 2889 2890 Xinha.prototype._splitBlock = function() 2891 { 2892 this._doc.execCommand('formatblock', false, 'div'); 2893 }; 2894 2895 Xinha.prototype.forceRedraw = function() 2896 { 2897 this._doc.body.style.visibility = "hidden"; 2898 this._doc.body.style.visibility = "visible"; 2899 // this._doc.body.innerHTML = this.getInnerHTML(); 2900 }; 2901 2902 // focuses the iframe window. returns a reference to the editor document. 2903 Xinha.prototype.focusEditor = function() 2904 { 2905 switch (this._editMode) 2906 { 2907 // notice the try { ... } catch block to avoid some rare exceptions in FireFox 2908 // (perhaps also in other Gecko browsers). Manual focus by user is required in 2909 // case of an error. Somebody has an idea? 2910 case "wysiwyg" : 2911 try 2912 { 2913 // We don't want to focus the field unless at least one field has been activated. 2914 if ( Xinha._someEditorHasBeenActivated ) 2915 { 2916 this.activateEditor(); // Ensure *this* editor is activated 2917 this._iframe.contentWindow.focus(); // and focus it 2918 } 2919 } catch (ex) {} 2920 break; 2921 case "textmode": 2922 try 2923 { 2924 this._textArea.focus(); 2925 } catch (e) {} 2926 break; 2927 default: 2928 alert("ERROR: mode " + this._editMode + " is not defined"); 2929 } 2930 return this._doc; 2931 }; 2932 2933 // takes a snapshot of the current text (for undo) 2934 Xinha.prototype._undoTakeSnapshot = function() 2935 { 2936 ++this._undoPos; 2937 if ( this._undoPos >= this.config.undoSteps ) 2938 { 2939 // remove the first element 2940 this._undoQueue.shift(); 2941 --this._undoPos; 2942 } 2943 // use the fasted method (getInnerHTML); 2944 var take = true; 2945 var txt = this.getInnerHTML(); 2946 if ( this._undoPos > 0 ) 2947 { 2948 take = (this._undoQueue[this._undoPos - 1] != txt); 2949 } 2950 if ( take ) 2951 { 2952 this._undoQueue[this._undoPos] = txt; 2953 } 2954 else 2955 { 2956 this._undoPos--; 2957 } 2958 }; 2959 2960 Xinha.prototype.undo = function() 2961 { 2962 if ( this._undoPos > 0 ) 2963 { 2964 var txt = this._undoQueue[--this._undoPos]; 2965 if ( txt ) 2966 { 2967 this.setHTML(txt); 2968 } 2969 else 2970 { 2971 ++this._undoPos; 2972 } 2973 } 2974 }; 2975 2976 Xinha.prototype.redo = function() 2977 { 2978 if ( this._undoPos < this._undoQueue.length - 1 ) 2979 { 2980 var txt = this._undoQueue[++this._undoPos]; 2981 if ( txt ) 2982 { 2983 this.setHTML(txt); 2984 } 2985 else 2986 { 2987 --this._undoPos; 2988 } 2989 } 2990 }; 2991 2992 Xinha.prototype.disableToolbar = function(except) 2993 { 2994 if ( this._timerToolbar ) 2995 { 2996 clearTimeout(this._timerToolbar); 2997 } 2998 if ( typeof except == 'undefined' ) 2999 { 3000 except = [ ]; 3001 } 3002 else if ( typeof except != 'object' ) 3003 { 3004 except = [except]; 3005 } 3006 3007 for ( var i in this._toolbarObjects ) 3008 { 3009 var btn = this._toolbarObjects[i]; 3010 if ( except.contains(i) ) 3011 { 3012 continue; 3013 } 3014 // prevent iterating over wrong type 3015 if ( typeof(btn.state) != 'function' ) 3016 { 3017 continue; 3018 } 3019 btn.state("enabled", false); 3020 } 3021 }; 3022 3023 Xinha.prototype.enableToolbar = function() 3024 { 3025 this.updateToolbar(); 3026 }; 3027 3028 if ( !Array.prototype.contains ) 3029 { 3030 Array.prototype.contains = function(needle) 3031 { 3032 var haystack = this; 3033 for ( var i = 0; i < haystack.length; i++ ) 3034 { 3035 if ( needle == haystack[i] ) 3036 { 3037 return true; 3038 } 3039 } 3040 return false; 3041 }; 3042 } 3043 3044 if ( !Array.prototype.indexOf ) 3045 { 3046 Array.prototype.indexOf = function(needle) 3047 { 3048 var haystack = this; 3049 for ( var i = 0; i < haystack.length; i++ ) 3050 { 3051 if ( needle == haystack[i] ) 3052 { 3053 return i; 3054 } 3055 } 3056 return null; 3057 }; 3058 } 3059 3060 // FIXME : this function needs to be splitted in more functions. 3061 // It is actually to heavy to be understable and very scary to manipulate 3062 // updates enabled/disable/active state of the toolbar elements 3063 Xinha.prototype.updateToolbar = function(noStatus) 3064 { 3065 var doc = this._doc; 3066 var text = (this._editMode == "textmode"); 3067 var ancestors = null; 3068 if ( !text ) 3069 { 3070 ancestors = this.getAllAncestors(); 3071 if ( this.config.statusBar && !noStatus ) 3072 { 3073 this._statusBarTree.innerHTML = Xinha._lc("Path") + ": "; // clear 3074 for ( var i = ancestors.length; --i >= 0; ) 3075 { 3076 var el = ancestors[i]; 3077 if ( !el ) 3078 { 3079 // hell knows why we get here; this 3080 // could be a classic example of why 3081 // it's good to check for conditions 3082 // that are impossible to happen ;-) 3083 continue; 3084 } 3085 var a = document.createElement("a"); 3086 a.href = "javascript:void(0)"; 3087 a.el = el; 3088 a.editor = this; 3089 Xinha.addDom0Event( 3090 a, 3091 'click', 3092 function() { 3093 this.blur(); 3094 this.editor.selectNodeContents(this.el); 3095 this.editor.updateToolbar(true); 3096 return false; 3097 } 3098 ); 3099 Xinha.addDom0Event( 3100 a, 3101 'contextmenu', 3102 function() 3103 { 3104 // TODO: add context menu here 3105 this.blur(); 3106 var info = "Inline style:\n\n"; 3107 info += this.el.style.cssText.split(/;\s*/).join(";\n"); 3108 alert(info); 3109 return false; 3110 } 3111 ); 3112 var txt = el.tagName.toLowerCase(); 3113 if (typeof el.style != 'undefined') a.title = el.style.cssText; 3114 if ( el.id ) 3115 { 3116 txt += "#" + el.id; 3117 } 3118 if ( el.className ) 3119 { 3120 txt += "." + el.className; 3121 } 3122 a.appendChild(document.createTextNode(txt)); 3123 this._statusBarTree.appendChild(a); 3124 if ( i !== 0 ) 3125 { 3126 this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb))); 3127 } 3128 } 3129 } 3130 } 3131 3132 for ( var cmd in this._toolbarObjects ) 3133 { 3134 var btn = this._toolbarObjects[cmd]; 3135 var inContext = true; 3136 // prevent iterating over wrong type 3137 if ( typeof(btn.state) != 'function' ) 3138 { 3139 continue; 3140 } 3141 if ( btn.context && !text ) 3142 { 3143 inContext = false; 3144 var context = btn.context; 3145 var attrs = []; 3146 if ( /(.*)\[(.*?)\]/.test(context) ) 3147 { 3148 context = RegExp.$1; 3149 attrs = RegExp.$2.split(","); 3150 } 3151 context = context.toLowerCase(); 3152 var match = (context == "*"); 3153 for ( var k = 0; k < ancestors.length; ++k ) 3154 { 3155 if ( !ancestors[k] ) 3156 { 3157 // the impossible really happens. 3158 continue; 3159 } 3160 if ( match || ( ancestors[k].tagName.toLowerCase() == context ) ) 3161 { 3162 inContext = true; 3163 for ( var ka = 0; ka < attrs.length; ++ka ) 3164 { 3165 if (!ancestors[k].getAttribute(attrs[ka])) 3166 { 3167 inContext = false; 3168 break; 3169 } 3170 } 3171 if ( inContext ) 3172 { 3173 break; 3174 } 3175 } 3176 } 3177 } 3178 btn.state("enabled", (!text || btn.text) && inContext); 3179 if ( typeof cmd == "function" ) 3180 { 3181 continue; 3182 } 3183 // look-it-up in the custom dropdown boxes 3184 var dropdown = this.config.customSelects[cmd]; 3185 if ( ( !text || btn.text ) && ( typeof dropdown != "undefined" ) ) 3186 { 3187 dropdown.refresh(this); 3188 continue; 3189 } 3190 switch (cmd) 3191 { 3192 case "fontname": 3193 case "fontsize": 3194 if ( !text ) 3195 { 3196 try 3197 { 3198 var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); 3199 if ( !value ) 3200 { 3201 btn.element.selectedIndex = 0; 3202 break; 3203 } 3204 3205 // HACK -- retrieve the config option for this 3206 // combo box. We rely on the fact that the 3207 // variable in config has the same name as 3208 // button name in the toolbar. 3209 var options = this.config[cmd]; 3210 var sIndex = 0; 3211 for ( var j in options ) 3212 { 3213 // FIXME: the following line is scary. 3214 if ( ( j.toLowerCase() == value ) || ( options[j].substr(0, value.length).toLowerCase() == value ) ) 3215 { 3216 btn.element.selectedIndex = sIndex; 3217 throw "ok"; 3218 } 3219 ++sIndex; 3220 } 3221 btn.element.selectedIndex = 0; 3222 } catch(ex) {} 3223 } 3224 break; 3225 3226 // It's better to search for the format block by tag name from the 3227 // current selection upwards, because IE has a tendancy to return 3228 // things like 'heading 1' for 'h1', which breaks things if you want 3229 // to call your heading blocks 'header 1'. Stupid MS. 3230 case "formatblock": 3231 var blocks = []; 3232 for ( var indexBlock in this.config.formatblock ) 3233 { 3234 // prevent iterating over wrong type 3235 if ( typeof this.config.formatblock[indexBlock] == 'string' ) 3236 { 3237 blocks[blocks.length] = this.config.formatblock[indexBlock]; 3238 } 3239 } 3240 3241 var deepestAncestor = this._getFirstAncestor(this._getSelection(), blocks); 3242 if ( deepestAncestor ) 3243 { 3244 for ( var x = 0; x < blocks.length; x++ ) 3245 { 3246 if ( blocks[x].toLowerCase() == deepestAncestor.tagName.toLowerCase() ) 3247 { 3248 btn.element.selectedIndex = x; 3249 } 3250 } 3251 } 3252 else 3253 { 3254 btn.element.selectedIndex = 0; 3255 } 3256 break; 3257 3258 case "textindicator": 3259 if ( !text ) 3260 { 3261 try 3262 { 3263 var style = btn.element.style; 3264 style.backgroundColor = Xinha._makeColor(doc.queryCommandValue(Xinha.is_ie ? "backcolor" : "hilitecolor")); 3265 if ( /transparent/i.test(style.backgroundColor) ) 3266 { 3267 // Mozilla 3268 style.backgroundColor = Xinha._makeColor(doc.queryCommandValue("backcolor")); 3269 } 3270 style.color = Xinha._makeColor(doc.queryCommandValue("forecolor")); 3271 style.fontFamily = doc.queryCommandValue("fontname"); 3272 style.fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; 3273 style.fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; 3274 } catch (ex) { 3275 // alert(e + "\n\n" + cmd); 3276 } 3277 } 3278 break; 3279 3280 case "htmlmode": 3281 btn.state("active", text); 3282 break; 3283 3284 case "lefttoright": 3285 case "righttoleft": 3286 var eltBlock = this.getParentElement(); 3287 while ( eltBlock && !Xinha.isBlockElement(eltBlock) ) 3288 { 3289 eltBlock = eltBlock.parentNode; 3290 } 3291 if ( eltBlock ) 3292 { 3293 btn.state("active", (eltBlock.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr"))); 3294 } 3295 break; 3296 3297 default: 3298 cmd = cmd.replace(/(un)?orderedlist/i, "insert$1orderedlist"); 3299 try 3300 { 3301 btn.state("active", (!text && doc.queryCommandState(cmd))); 3302 } catch (ex) {} 3303 break; 3304 } 3305 } 3306 // take undo snapshots 3307 if ( this._customUndo && !this._timerUndo ) 3308 { 3309 this._undoTakeSnapshot(); 3310 var editor = this; 3311 this._timerUndo = setTimeout(function() { editor._timerUndo = null; }, this.config.undoTimeout); 3312 } 3313 3314 // Insert a space in certain locations, this is just to make editing a little 3315 // easier (to "get out of" tags), it's not essential. 3316 // TODO: Make this work for IE? 3317 // TODO: Perhaps should use a plain space character, I'm not sure. 3318 // OK, I've disabled this temporarily, to be honest, I can't rightly remember what the 3319 // original problem was I was trying to solve with it. I think perhaps that EnterParagraphs 3320 // might solve the problem, whatever the hell it was. I'm going senile, I'm sure. 3321 // @todo : since this part is disabled since a long time, does it still need to be in the source ? 3322 if( 0 && Xinha.is_gecko ) 3323 { 3324 var s = this._getSelection(); 3325 // If the last character in the last text node of the parent tag 3326 // and the parent tag is not a block tag 3327 if ( s && s.isCollapsed && s.anchorNode && 3328 s.anchorNode.parentNode.tagName.toLowerCase() != 'body' && 3329 s.anchorNode.nodeType == 3 && s.anchorOffset == s.anchorNode.length && 3330 !( s.anchorNode.parentNode.nextSibling && s.anchorNode.parentNode.nextSibling.nodeType == 3 ) && 3331 !Xinha.isBlockElement(s.anchorNode.parentNode) ) 3332 { 3333 // Insert hair-width-space after the close tag if there isn't another text node on the other side 3334 // It could also work with zero-width-space (\u200B) but I don't like it so much. 3335 // Perhaps this won't work well in various character sets and we should use plain space (20)? 3336 try 3337 { 3338 s.anchorNode.parentNode.parentNode.insertBefore(this._doc.createTextNode('\t'), s.anchorNode.parentNode.nextSibling); 3339 } 3340 catch(ex) {} // Disregard 3341 } 3342 } 3343 3344 // check if any plugins have registered refresh handlers 3345 for ( var indexPlugin in this.plugins ) 3346 { 3347 var plugin = this.plugins[indexPlugin].instance; 3348 if ( plugin && typeof plugin.onUpdateToolbar == "function" ) 3349 { 3350 plugin.onUpdateToolbar(); 3351 } 3352 } 3353 3354 }; 3355 3356 3357 // moved Xinha.prototype.insertNodeAtSelection() to browser specific file 3358 // moved Xinha.prototype.getParentElement() to browser specific file 3359 3360 // Returns an array with all the ancestor nodes of the selection. 3361 Xinha.prototype.getAllAncestors = function() 3362 { 3363 var p = this.getParentElement(); 3364 var a = []; 3365 while ( p && (p.nodeType == 1) && ( p.tagName.toLowerCase() != 'body' ) ) 3366 { 3367 a.push(p); 3368 p = p.parentNode; 3369 } 3370 a.push(this._doc.body); 3371 return a; 3372 }; 3373 3374 // Returns the deepest ancestor of the selection that is of the current type 3375 Xinha.prototype._getFirstAncestor = function(sel, types) 3376 { 3377 var prnt = this._activeElement(sel); 3378 if ( prnt === null ) 3379 { 3380 try 3381 { 3382 prnt = (Xinha.is_ie ? this._createRange(sel).parentElement() : this._createRange(sel).commonAncestorContainer); 3383 } 3384 catch(ex) 3385 { 3386 return null; 3387 } 3388 } 3389 3390 if ( typeof types == 'string' ) 3391 { 3392 types = [types]; 3393 } 3394 3395 while ( prnt ) 3396 { 3397 if ( prnt.nodeType == 1 ) 3398 { 3399 if ( types === null ) 3400 { 3401 return prnt; 3402 } 3403 if ( types.contains(prnt.tagName.toLowerCase()) ) 3404 { 3405 return prnt; 3406 } 3407 if ( prnt.tagName.toLowerCase() == 'body' ) 3408 { 3409 break; 3410 } 3411 if ( prnt.tagName.toLowerCase() == 'table' ) 3412 { 3413 break; 3414 } 3415 } 3416 prnt = prnt.parentNode; 3417 } 3418 3419 return null; 3420 }; 3421 3422 // moved Xinha.prototype._activeElement() to browser specific file 3423 // moved Xinha.prototype._selectionEmpty() to browser specific file 3424 3425 Xinha.prototype._getAncestorBlock = function(sel) 3426 { 3427 // Scan upwards to find a block level element that we can change or apply to 3428 var prnt = (Xinha.is_ie ? this._createRange(sel).parentElement : this._createRange(sel).commonAncestorContainer); 3429 3430 while ( prnt && ( prnt.nodeType == 1 ) ) 3431 { 3432 switch ( prnt.tagName.toLowerCase() ) 3433 { 3434 case 'div': 3435 case 'p': 3436 case 'address': 3437 case 'blockquote': 3438 case 'center': 3439 case 'del': 3440 case 'ins': 3441 case 'pre': 3442 case 'h1': 3443 case 'h2': 3444 case 'h3': 3445 case 'h4': 3446 case 'h5': 3447 case 'h6': 3448 case 'h7': 3449 // Block Element 3450 return prnt; 3451 3452 case 'body': 3453 case 'noframes': 3454 case 'dd': 3455 case 'li': 3456 case 'th': 3457 case 'td': 3458 case 'noscript' : 3459 // Halting element (stop searching) 3460 return null; 3461 3462 default: 3463 // Keep lookin 3464 break; 3465 } 3466 } 3467 3468 return null; 3469 }; 3470 3471 Xinha.prototype._createImplicitBlock = function(type) 3472 { 3473 // expand it until we reach a block element in either direction 3474 // then wrap the selection in a block and return 3475 var sel = this._getSelection(); 3476 if ( Xinha.is_ie ) 3477 { 3478 sel.empty(); 3479 } 3480 else 3481 { 3482 sel.collapseToStart(); 3483 } 3484 3485 var rng = this._createRange(sel); 3486 3487 // Expand UP 3488 3489 // Expand DN 3490 }; 3491 3492 3493 // moved Xinha.prototype.selectNodeContents() to browser specific file 3494 // moved Xinha.prototype.insertHTML() to browser specific file 3495 3496 /** 3497 * Call this function to surround the existing HTML code in the selection with 3498 * your tags. FIXME: buggy! This function will be deprecated "soon". 3499 * @todo: when will it be deprecated ? Can it be removed already ? 3500 */ 3501 Xinha.prototype.surroundHTML = function(startTag, endTag) 3502 { 3503 var html = this.getSelectedHTML(); 3504 // the following also deletes the selection 3505 this.insertHTML(startTag + html + endTag); 3506 }; 3507 3508 // moved Xinha.prototype.getSelectedHTML() to browser specific file 3509 3510 /// Return true if we have some selection 3511 Xinha.prototype.hasSelectedText = function() 3512 { 3513 // FIXME: come _on_ mishoo, you can do better than this ;-) 3514 return this.getSelectedHTML() !== ''; 3515 }; 3516 3517 // moved Xinha.prototype._createLink() to popups/link.js 3518 // moved Xinha.prototype._insertImage() to popups/insert_image.js 3519 3520 // Called when the user clicks the Insert Table button 3521 Xinha.prototype._insertTable = function() 3522 { 3523 var sel = this._getSelection(); 3524 var range = this._createRange(sel); 3525 var editor = this; // for nested functions 3526 this._popupDialog( 3527 editor.config.URIs.insert_table, 3528 function(param) 3529 { 3530 // user must have pressed Cancel 3531 if ( !param ) 3532 { 3533 return false; 3534 } 3535 var doc = editor._doc; 3536 // create the table element 3537 var table = doc.createElement("table"); 3538 // assign the given arguments 3539 3540 for ( var field in param ) 3541 { 3542 var value = param[field]; 3543 if ( !value ) 3544 { 3545 continue; 3546 } 3547 switch (field) 3548 { 3549 case "f_width": 3550 table.style.width = value + param.f_unit; 3551 break; 3552 case "f_align": 3553 table.align = value; 3554 break; 3555 case "f_border": 3556 table.border = parseInt(value, 10); 3557 break; 3558 case "f_spacing": 3559 table.cellSpacing = parseInt(value, 10); 3560 break; 3561 case "f_padding": 3562 table.cellPadding = parseInt(value, 10); 3563 break; 3564 } 3565 } 3566 var cellwidth = 0; 3567 if ( param.f_fixed ) 3568 { 3569 cellwidth = Math.floor(100 / parseInt(param.f_cols, 10)); 3570 } 3571 var tbody = doc.createElement("tbody"); 3572 table.appendChild(tbody); 3573 for ( var i = 0; i < param.f_rows; ++i ) 3574 { 3575 var tr = doc.createElement("tr"); 3576 tbody.appendChild(tr); 3577 for ( var j = 0; j < param.f_cols; ++j ) 3578 { 3579 var td = doc.createElement("td"); 3580 // @todo : check if this line doesnt stop us to use pixel width in cells 3581 if (cellwidth) 3582 { 3583 td.style.width = cellwidth + "%"; 3584 } 3585 tr.appendChild(td); 3586 // Browsers like to see something inside the cell ( ). 3587 td.appendChild(doc.createTextNode('\u00a0')); 3588 } 3589 } 3590 if ( Xinha.is_ie ) 3591 { 3592 range.pasteHTML(table.outerHTML); 3593 } 3594 else 3595 { 3596 // insert the table 3597 editor.insertNodeAtSelection(table); 3598 } 3599 return true; 3600 }, 3601 null 3602 ); 3603 }; 3604 3605 /*************************************************** 3606 * Category: EVENT HANDLERS 3607 ***************************************************/ 3608 3609 // el is reference to the SELECT object 3610 // txt is the name of the select field, as in config.toolbar 3611 Xinha.prototype._comboSelected = function(el, txt) 3612 { 3613 this.focusEditor(); 3614 var value = el.options[el.selectedIndex].value; 3615 switch (txt) 3616 { 3617 case "fontname": 3618 case "fontsize": 3619 this.execCommand(txt, false, value); 3620 break; 3621 case "formatblock": 3622 // Mozilla inserts an empty tag (<>) if no parameter is passed 3623 if ( !value ) 3624 { 3625 this.updateToolbar(); 3626 break; 3627 } 3628 if( !Xinha.is_gecko || value !== 'blockquote' ) 3629 { 3630 value = "<" + value + ">"; 3631 } 3632 this.execCommand(txt, false, value); 3633 break; 3634 default: 3635 // try to look it up in the registered dropdowns 3636 var dropdown = this.config.customSelects[txt]; 3637 if ( typeof dropdown != "undefined" ) 3638 { 3639 dropdown.action(this); 3640 } 3641 else 3642 { 3643 alert("FIXME: combo box " + txt + " not implemented"); 3644 } 3645 break; 3646 } 3647 }; 3648 3649 /** 3650 * Open a popup to select the hilitecolor or forecolor 3651 * 3652 * @param {String} cmdID The commande ID (hilitecolor or forecolor) 3653 * @private 3654 */ 3655 Xinha.prototype._colorSelector = function(cmdID) 3656 { 3657 var editor = this; // for nested functions 3658 3659 if ( typeof colorPicker == 'undefined' ) 3660 { 3661 Xinha._loadback(_editor_url + 'popups/color_picker.js', function () {editor._colorSelector(cmdID)}); 3662 return false; 3663 } 3664 3665 var btn = editor._toolbarObjects[cmdID].element; 3666 var initcolor; 3667 if ( cmdID == 'hilitecolor' ) 3668 { 3669 if ( Xinha.is_ie ) 3670 { 3671 cmdID = 'backcolor'; 3672 initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("backcolor")); 3673 } 3674 else 3675 { 3676 initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("hilitecolor")); 3677 } 3678 // @todo : useCSS is deprecated, see ticket #619 3679 //[wymsy]: mozilla bug #279330 has been fixed, I don't think we need this any more 3680 /* if ( Xinha.is_gecko ) 3681 { 3682 try 3683 { 3684 // editor._doc.execCommand('useCSS', false, false); //switch on useCSS (mozilla bug #279330) 3685 } catch (ex) {} 3686 }*/ 3687 } 3688 else 3689 { 3690 initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("forecolor")); 3691 } 3692 var cback = function(color) { editor._doc.execCommand(cmdID, false, color); }; 3693 if ( Xinha.is_ie ) 3694 { 3695 var range = editor._createRange(editor._getSelection()); 3696 cback = function(color) 3697 { 3698 range.select(); 3699 editor._doc.execCommand(cmdID, false, color); 3700 }; 3701 } 3702 var picker = new colorPicker( 3703 { 3704 cellsize:editor.config.colorPickerCellSize, 3705 callback:cback, 3706 granularity:editor.config.colorPickerGranularity, 3707 websafe:editor.config.colorPickerWebSafe, 3708 savecolors:editor.config.colorPickerSaveColors 3709 }); 3710 picker.open(editor.config.colorPickerPosition, btn, initcolor); 3711 }; 3712 3713 // the execCommand function (intercepts some commands and replaces them with 3714 // our own implementation) 3715 Xinha.prototype.execCommand = function(cmdID, UI, param) 3716 { 3717 var editor = this; // for nested functions 3718 this.focusEditor(); 3719 cmdID = cmdID.toLowerCase(); 3720 // @todo : useCSS is deprecated, see ticket #619 3721 if ( Xinha.is_gecko ) 3722 { 3723 try 3724 { 3725 this._doc.execCommand('useCSS', false, true); //switch useCSS off (true=off) 3726 } catch (ex) {} 3727 } 3728 switch (cmdID) 3729 { 3730 case "htmlmode": 3731 this.setMode(); 3732 break; 3733 3734 case "hilitecolor": 3735 case "forecolor": 3736 this._colorSelector(cmdID); 3737 break; 3738 3739 case "createlink": 3740 this._createLink(); 3741 break; 3742 3743 case "undo": 3744 case "redo": 3745 if (this._customUndo) 3746 { 3747 this[cmdID](); 3748 } 3749 else 3750 { 3751 this._doc.execCommand(cmdID, UI, param); 3752 } 3753 break; 3754 3755 case "inserttable": 3756 this._insertTable(); 3757 break; 3758 3759 case "insertimage": 3760 this._insertImage(); 3761 break; 3762 3763 case "about": 3764 this._popupDialog(editor.config.URIs.about, null, this); 3765 break; 3766 3767 case "showhelp": 3768 this._popupDialog(editor.config.URIs.help, null, this); 3769 break; 3770 3771 case "killword": 3772 this._wordClean(); 3773 break; 3774 3775 case "cut": 3776 case "copy": 3777 case "paste": 3778 try 3779 { 3780 this._doc.execCommand(cmdID, UI, param); 3781 if ( this.config.killWordOnPaste ) 3782 { 3783 this._wordClean(); 3784 } 3785 } 3786 catch (ex) 3787 { 3788 if ( Xinha.is_gecko ) 3789 { 3790 alert(Xinha._lc("The Paste button does not work in Mozilla based web browsers (technical security reasons). Press CTRL-V on your keyboard to paste directly.")); 3791 } 3792 } 3793 break; 3794 case "lefttoright": 3795 case "righttoleft": 3796 if (this.config.changeJustifyWithDirection) 3797 { 3798 this._doc.execCommand((cmdID == "righttoleft") ? "justifyright" : "justifyleft", UI, param); 3799 } 3800 var dir = (cmdID == "righttoleft") ? "rtl" : "ltr"; 3801 var el = this.getParentElement(); 3802 while ( el && !Xinha.isBlockElement(el) ) 3803 { 3804 el = el.parentNode; 3805 } 3806 if ( el ) 3807 { 3808 if ( el.style.direction == dir ) 3809 { 3810 el.style.direction = ""; 3811 } 3812 else 3813 { 3814 el.style.direction = dir; 3815 } 3816 } 3817 break; 3818 default: 3819 try 3820 { 3821 this._doc.execCommand(cmdID, UI, param); 3822 } 3823 catch(ex) 3824 { 3825 if ( this.config.debug ) 3826 { 3827 alert(e + "\n\nby execCommand(" + cmdID + ");"); 3828 } 3829 } 3830 break; 3831 } 3832 3833 this.updateToolbar(); 3834 return false; 3835 }; 3836 3837 /** A generic event handler for things that happen in the IFRAME's document. 3838 * @todo: this function is *TOO* generic, it needs to be splitted in more specific handlers 3839 * This function also handles key bindings. */ 3840 Xinha.prototype._editorEvent = function(ev) 3841 { 3842 var editor = this; 3843 var keyEvent = (Xinha.is_ie && ev.type == "keydown") || (!Xinha.is_ie && ev.type == "keypress"); 3844 3845 //call events of textarea 3846 if ( typeof editor._textArea['on'+ev.type] == "function" ) 3847 { 3848 editor._textArea['on'+ev.type](); 3849 } 3850 3851 if ( Xinha.is_gecko && keyEvent && ev.ctrlKey && this._unLink && this._unlinkOnUndo ) 3852 { 3853 if ( String.fromCharCode(ev.charCode).toLowerCase() == 'z' ) 3854 { 3855 Xinha._stopEvent(ev); 3856 this._unLink(); 3857 editor.updateToolbar(); 3858 return; 3859 } 3860 } 3861 3862 if ( keyEvent ) 3863 { 3864 for ( var i in editor.plugins ) 3865 { 3866 var plugin = editor.plugins[i].instance; 3867 if ( plugin && typeof plugin.onKeyPress == "function" ) 3868 { 3869 if ( plugin.onKeyPress(ev) ) 3870 { 3871 return false; 3872 } 3873 } 3874 } 3875 } 3876 3877 if ( keyEvent && ev.ctrlKey && !ev.altKey ) 3878 { 3879 this._shortCuts(ev); 3880 } 3881 else if ( keyEvent && Xinha.is_gecko ) 3882 { 3883 this.mozKey( ev, keyEvent ); 3884 } 3885 3886 // update the toolbar state after some time 3887 if ( editor._timerToolbar ) 3888 { 3889 clearTimeout(editor._timerToolbar); 3890 } 3891 editor._timerToolbar = setTimeout( 3892 function() 3893 { 3894 editor.updateToolbar(); 3895 editor._timerToolbar = null; 3896 }, 3897 250); 3898 }; 3899 3900 // handles ctrl + key shortcuts 3901 Xinha.prototype._shortCuts = function (ev) 3902 { 3903 var editor = this; 3904 var sel = null; 3905 var range = null; 3906 var key = String.fromCharCode(Xinha.is_ie ? ev.keyCode : ev.charCode).toLowerCase(); 3907 var cmd = null; 3908 var value = null; 3909 switch (key) 3910 { 3911 case 'a': 3912 if ( !Xinha.is_ie ) 3913 { 3914 // KEY select all 3915 sel = this._getSelection(); 3916 sel.removeAllRanges(); 3917 range = this._createRange(); 3918 range.selectNodeContents(this._doc.body); 3919 sel.addRange(range); 3920 Xinha._stopEvent(ev); 3921 } 3922 break; 3923 3924 // simple key commands follow 3925 3926 case 'b': cmd = "bold"; break; 3927 case 'i': cmd = "italic"; break; 3928 case 'u': cmd = "underline"; break; 3929 case 's': cmd = "strikethrough"; break; 3930 case 'l': cmd = "justifyleft"; break; 3931 case 'e': cmd = "justifycenter"; break; 3932 case 'r': cmd = "justifyright"; break; 3933 case 'j': cmd = "justifyfull"; break; 3934 case 'z': cmd = "undo"; break; 3935 case 'y': cmd = "redo"; break; 3936 case 'v': 3937 if ( Xinha.is_ie || editor.config.htmlareaPaste ) 3938 { 3939 cmd = "paste"; 3940 } 3941 break; 3942 case 'n': 3943 cmd = "formatblock"; 3944 value = Xinha.is_ie ? "<p>" : "p"; 3945 break; 3946 3947 case '0': cmd = "killword"; break; 3948 3949 // headings 3950 case '1': 3951 case '2': 3952 case '3': 3953 case '4': 3954 case '5': 3955 case '6': 3956 cmd = "formatblock"; 3957 value = "h" + key; 3958 if ( Xinha.is_ie ) 3959 { 3960 value = "<" + value + ">"; 3961 } 3962 break; 3963 } 3964 if ( cmd ) 3965 { 3966 // execute simple command 3967 this.execCommand(cmd, false, value); 3968 Xinha._stopEvent(ev); 3969 } 3970 }; 3971 3972 Xinha.prototype.convertNode = function(el, newTagName) 3973 { 3974 var newel = this._doc.createElement(newTagName); 3975 while ( el.firstChild ) 3976 { 3977 newel.appendChild(el.firstChild); 3978 } 3979 return newel; 3980 }; 3981 3982 // moved Xinha.prototype.checkBackspace() to browser specific file 3983 3984 /** The idea here is 3985 * 1. See if we are in a block element 3986 * 2. If we are not, then wrap the current "block" of text into a paragraph 3987 * 3. Now that we have a block element, select all the text between the insertion point 3988 * and just AFTER the end of the block 3989 * eg <p>The quick |brown fox jumped over the lazy dog.</p>| 3990 * --------------------------------------- 3991 * 4. Extract that from the document, making 3992 * <p>The quick </p> 3993 * and a document fragment with 3994 * <p>brown fox jumped over the lazy dog.</p> 3995 * 5. Reinsert it just after the block element 3996 * <p>The quick </p><p>brown fox jumped over the lazy dog.</p> 3997 * 3998 * Along the way, allow inserting blank paragraphs, which will look like <p><br/></p> 3999 */ 4000 4001 Xinha.prototype.dom_checkInsertP = function() 4002 { 4003 var p, body; 4004 // Get the insertion point, we'll scrub any highlighted text the user wants rid of while we are there. 4005 var sel = this._getSelection(); 4006 var range = this._createRange(sel); 4007 if ( !range.collapsed ) 4008 { 4009 range.deleteContents(); 4010 } 4011 this.deactivateEditor(); 4012 //sel.removeAllRanges(); 4013 //sel.addRange(range); 4014 4015 var SC = range.startContainer; 4016 var SO = range.startOffset; 4017 var EC = range.endContainer; 4018 var EO = range.endOffset; 4019 4020 // If the insertion point is character 0 of the 4021 // document, then insert a space character that we will wrap into a paragraph 4022 // in a bit. 4023 if ( SC == EC && SC == body && !SO && !EO ) 4024 { 4025 p = this._doc.createTextNode(" "); 4026 body.insertBefore(p, body.firstChild); 4027 range.selectNodeContents(p); 4028 SC = range.startContainer; 4029 SO = range.startOffset; 4030 EC = range.endContainer; 4031 EO = range.endOffset; 4032 } 4033 4034 // See if we are in a block element, if so, great. 4035 p = this.getAllAncestors(); 4036 4037 var block = null; 4038 body = this._doc.body; 4039 for ( var i = 0; i < p.length; ++i ) 4040 { 4041 if ( Xinha.isParaContainer(p[i]) ) 4042 { 4043 break; 4044 } 4045 else if ( Xinha.isBlockElement(p[i]) && ! ( /body|html/i.test(p[i].tagName) ) ) 4046 { 4047 block = p[i]; 4048 break; 4049 } 4050 } 4051 4052 // If not in a block element, we'll have to turn some stuff into a paragraph 4053 if ( !block ) 4054 { 4055 // We want to wrap as much stuff as possible into the paragraph in both directions 4056 // from the insertion point. We start with the start container and walk back up to the 4057 // node just before any of the paragraph containers. 4058 var wrap = range.startContainer; 4059 while ( wrap.parentNode && !Xinha.isParaContainer(wrap.parentNode) ) 4060 { 4061 wrap = wrap.parentNode; 4062 } 4063 var start = wrap; 4064 var end = wrap; 4065 4066 // Now we walk up the sibling list until we hit the top of the document 4067 // or an element that we shouldn't put in a p (eg other p, div, ul, ol, table) 4068 while ( start.previousSibling ) 4069 { 4070 if ( start.previousSibling.tagName ) 4071 { 4072 if ( !Xinha.isBlockElement(start.previousSibling) ) 4073 { 4074 start = start.previousSibling; 4075 } 4076 else 4077 { 4078 break; 4079 } 4080 } 4081 else 4082 { 4083 start = start.previousSibling; 4084 } 4085 } 4086 4087 // Same down the list 4088 while ( end.nextSibling ) 4089 { 4090 if ( end.nextSibling.tagName ) 4091 { 4092 if ( !Xinha.isBlockElement(end.nextSibling) ) 4093 { 4094 end = end.nextSibling; 4095 } 4096 else 4097 { 4098 break; 4099 } 4100 } 4101 else 4102 { 4103 end = end.nextSibling; 4104 } 4105 } 4106 4107 // Select the entire block 4108 range.setStartBefore(start); 4109 range.setEndAfter(end); 4110 4111 // Make it a paragraph 4112 range.surroundContents(this._doc.createElement('p')); 4113 4114 // Which becomes the block element 4115 block = range.startContainer.firstChild; 4116 4117 // And finally reset the insertion point to where it was originally 4118 range.setStart(SC, SO); 4119 } 4120 4121 // The start point is the insertion point, so just move the end point to immediatly 4122 // after the block 4123 range.setEndAfter(block); 4124 4125 // Extract the range, to split the block 4126 // If we just did range.extractContents() then Mozilla does wierd stuff 4127 // with selections, but if we clone, then remove the original range and extract 4128 // the clone, it's quite happy. 4129 var r2 = range.cloneRange(); 4130 sel.removeRange(range); 4131 var df = r2.extractContents(); 4132 4133 if ( df.childNodes.length === 0 ) 4134 { 4135 df.appendChild(this._doc.createElement('p')); 4136 df.firstChild.appendChild(this._doc.createElement('br')); 4137 } 4138 4139 if ( df.childNodes.length > 1 ) 4140 { 4141 var nb = this._doc.createElement('p'); 4142 while ( df.firstChild ) 4143 { 4144 var s = df.firstChild; 4145 df.removeChild(s); 4146 nb.appendChild(s); 4147 } 4148 df.appendChild(nb); 4149 } 4150 4151 // If the original block is empty, put a &nsbp; in it. 4152 // @fixme: why using a regex instead of : if (block.innerHTML.trim() == '') ? 4153 if ( ! ( /\S/.test(block.innerHTML) ) ) 4154 { 4155 block.innerHTML = " "; 4156 } 4157 4158 p = df.firstChild; 4159 // @fixme: why using a regex instead of : if (p.innerHTML.trim() == '') ? 4160 if ( ! ( /\S/.test(p.innerHTML) ) ) 4161 { 4162 p.innerHTML = "<br />"; 4163 } 4164 4165 // If the new block is empty and it's a heading, make it a paragraph 4166 // note, the new block is empty when you are hitting enter at the end of the existing block 4167 if ( ( /^\s*<br\s*\/?>\s*$/.test(p.innerHTML) ) && ( /^h[1-6]$/i.test(p.tagName) ) ) 4168 { 4169 df.appendChild(this.convertNode(p, "p")); 4170 df.removeChild(p); 4171 } 4172 4173 var newblock = block.parentNode.insertBefore(df.firstChild, block.nextSibling); 4174 4175 // Select the range (to set the insertion) 4176 // collapse to the start of the new block 4177 // (remember the block might be <p><br/></p>, so if we collapsed to the end the <br/> would be noticable) 4178 4179 //range.selectNode(newblock.firstChild); 4180 //range.collapse(true); 4181 4182 this.activateEditor(); 4183 4184 sel = this._getSelection(); 4185 sel.removeAllRanges(); 4186 sel.collapse(newblock,0); 4187 4188 // scroll into view 4189 this.scrollToElement(newblock); 4190 4191 //this.forceRedraw(); 4192 4193 }; 4194 4195 Xinha.prototype.scrollToElement = function(e) 4196 { 4197 if ( Xinha.is_gecko ) 4198 { 4199 var top = 0; 4200 var left = 0; 4201 while ( e ) 4202 { 4203 top += e.offsetTop; 4204 left += e.offsetLeft; 4205 if ( e.offsetParent && e.offsetParent.tagName.toLowerCase() != 'body' ) 4206 { 4207 e = e.offsetParent; 4208 } 4209 else 4210 { 4211 e = null; 4212 } 4213 } 4214 this._iframe.contentWindow.scrollTo(left, top); 4215 } 4216 }; 4217 4218 // retrieve the HTML 4219 Xinha.prototype.getHTML = function() 4220 { 4221 var html = ''; 4222 switch ( this._editMode ) 4223 { 4224 case "wysiwyg": 4225 if ( !this.config.fullPage ) 4226 { 4227 html = Xinha.getHTML(this._doc.body, false, this); 4228 } 4229 else 4230 { 4231 html = this.doctype + "\n" + Xinha.getHTML(this._doc.documentElement, true, this); 4232 } 4233 break; 4234 case "textmode": 4235 html = this._textArea.value; 4236 break; 4237 default: 4238 alert("Mode <" + this._editMode + "> not defined!"); 4239 return false; 4240 } 4241 return html; 4242 }; 4243 4244 Xinha.prototype.outwardHtml = function(html) 4245 { 4246 html = html.replace(/<(\/?)b(\s|>|\/)/ig, "<$1strong$2"); 4247 html = html.replace(/<(\/?)i(\s|>|\/)/ig, "<$1em$2"); 4248 html = html.replace(/<(\/?)strike(\s|>|\/)/ig, "<$1del$2"); 4249 4250 // replace window.open to that any clicks won't open a popup in designMode 4251 html = html.replace("onclick=\"try{if(document.designMode && document.designMode == 'on') return false;}catch(e){} window.open(", "onclick=\"window.open("); 4252 4253 // Figure out what our server name is, and how it's referenced 4254 var serverBase = location.href.replace(/(https?:\/\/[^\/]*)\/.*/, '$1') + '/'; 4255 4256 // IE puts this in can't figure out why 4257 html = html.replace(/https?:\/\/null\//g, serverBase); 4258 4259 // Make semi-absolute links to be truely absolute 4260 // we do this just to standardize so that special replacements knows what 4261 // to expect 4262 html = html.replace(/((href|src|background)=[\'\"])\/+/ig, '$1' + serverBase); 4263 4264 html = this.outwardSpecialReplacements(html); 4265 4266 html = this.fixRelativeLinks(html); 4267 4268 if ( this.config.sevenBitClean ) 4269 { 4270 html = html.replace(/[^ -~\r\n\t]/g, function(c) { return '&#'+c.charCodeAt(0)+';'; }); 4271 } 4272 4273 // ticket:56, the "greesemonkey" plugin for Firefox adds this junk, 4274 // so we strip it out. Original submitter gave a plugin, but that's 4275 // a bit much just for this IMHO - james 4276 if ( Xinha.is_gecko ) 4277 { 4278 html = html.replace(/<script[\s]*src[\s]*=[\s]*['"]chrome:\/\/.*?["']>[\s]*<\/script>/ig, ''); 4279 } 4280 //prevent execution of JavaScript (Ticket #685) 4281 html = html.replace(/(<script[^>]*)(freezescript)/gi,"$1javascript"); 4282 4283 return html; 4284 }; 4285 4286 Xinha.prototype.inwardHtml = function(html) 4287 { 4288 // Midas uses b and i instead of strong and em, um, hello, 4289 // mozilla, this is the 21st century calling! 4290 if ( Xinha.is_gecko ) 4291 { 4292 html = html.replace(/<(\/?)strong(\s|>|\/)/ig, "<$1b$2"); 4293 html = html.replace(/<(\/?)em(\s|>|\/)/ig, "<$1i$2"); 4294 } 4295 4296 // Both IE and Gecko use strike instead of del (#523) 4297 html = html.replace(/<(\/?)del(\s|>|\/)/ig, "<$1strike$2"); 4298 4299 // replace window.open to that any clicks won't open a popup in designMode 4300 html = html.replace("onclick=\"window.open(", "onclick=\"try{if(document.designMode && document.designMode == 'on') return false;}catch(e){} window.open("); 4301 4302 html = this.inwardSpecialReplacements(html); 4303 4304 html = html.replace(/(<script[^>]*)(javascript)/gi,"$1freezescript"); 4305 4306 // For IE's sake, make any URLs that are semi-absolute (="/....") to be 4307 // truely absolute 4308 var nullRE = new RegExp('((href|src|background)=[\'"])/+', 'gi'); 4309 html = html.replace(nullRE, '$1' + location.href.replace(/(https?:\/\/[^\/]*)\/.*/, '$1') + '/'); 4310 4311 html = this.fixRelativeLinks(html); 4312 return html; 4313 }; 4314 4315 Xinha.prototype.outwardSpecialReplacements = function(html) 4316 { 4317 for ( var i in this.config.specialReplacements ) 4318 { 4319 var from = this.config.specialReplacements[i]; 4320 var to = i; // why are declaring a new variable here ? Seems to be better to just do : for (var to in config) 4321 // prevent iterating over wrong type 4322 if ( typeof from.replace != 'function' || typeof to.replace != 'function' ) 4323 { 4324 continue; 4325 } 4326 // alert('out : ' + from + '=>' + to); 4327 var reg = new RegExp(from.replace(Xinha.RE_Specials, '\\$1'), 'g'); 4328 html = html.replace(reg, to.replace(/\$/g, '$$$$')); 4329 //html = html.replace(from, to); 4330 } 4331 return html; 4332 }; 4333 4334 Xinha.prototype.inwardSpecialReplacements = function(html) 4335 { 4336 // alert("inward"); 4337 for ( var i in this.config.specialReplacements ) 4338 { 4339 var from = i; // why are declaring a new variable here ? Seems to be better to just do : for (var from in config) 4340 var to = this.config.specialReplacements[i]; 4341 // prevent iterating over wrong type 4342 if ( typeof from.replace != 'function' || typeof to.replace != 'function' ) 4343 { 4344 continue; 4345 } 4346 // alert('in : ' + from + '=>' + to); 4347 // 4348 // html = html.replace(reg, to); 4349 // html = html.replace(from, to); 4350 var reg = new RegExp(from.replace(Xinha.RE_Specials, '\\$1'), 'g'); 4351 html = html.replace(reg, to.replace(/\$/g, '$$$$')); // IE uses doubled dollar signs to escape backrefs, also beware that IE also implements $& $_ and $' like perl. 4352 } 4353 return html; 4354 }; 4355 4356 4357 Xinha.prototype.fixRelativeLinks = function(html) 4358 { 4359 if ( typeof this.config.expandRelativeUrl != 'undefined' && this.config.expandRelativeUrl ) 4360 var src = html.match(/(src|href)="([^"]*)"/gi); 4361 var b = document.location.href; 4362 if ( src ) 4363 { 4364 var url,url_m,relPath,base_m,absPath 4365 for ( var i=0;i<src.length;++i ) 4366 { 4367 url = src[i].match(/(src|href)="([^"]*)"/i); 4368 url_m = url[2].match( /\.\.\//g ); 4369 if ( url_m ) 4370 { 4371 relPath = new RegExp( "(.*?)(([^\/]*\/){"+ url_m.length+"})[^\/]*$" ); 4372 base_m = b.match( relPath ); 4373 absPath = url[2].replace(/(\.\.\/)*/,base_m[1]); 4374 html = html.replace( new RegExp(url[2].replace( Xinha.RE_Specials, '\\$1' ) ),absPath ); 4375 } 4376 } 4377 } 4378 4379 if ( typeof this.config.stripSelfNamedAnchors != 'undefined' && this.config.stripSelfNamedAnchors ) 4380 { 4381 var stripRe = new RegExp(document.location.href.replace(/&/g,'&').replace(Xinha.RE_Specials, '\\$1') + '(#[^\'" ]*)', 'g'); 4382 html = html.replace(stripRe, '$1'); 4383 } 4384 4385 if ( typeof this.config.stripBaseHref != 'undefined' && this.config.stripBaseHref ) 4386 { 4387 var baseRe = null; 4388 if ( typeof this.config.baseHref != 'undefined' && this.config.baseHref !== null ) 4389 { 4390 baseRe = new RegExp( "((href|src|background)=\")(" + this.config.baseHref.replace( Xinha.RE_Specials, '\\$1' ) + ")", 'g' ); 4391 } 4392 else 4393 { 4394 baseRe = new RegExp( "((href|src|background)=\")(" + document.location.href.replace( /([^\/]*\/?)$/, '' ).replace( Xinha.RE_Specials, '\\$1' ) + ")", 'g' ); 4395 } 4396 4397 html = html.replace(baseRe, '$1'); 4398 } 4399 4400 // if ( Xinha.is_ie ) 4401 // { 4402 // This is now done in inward & outward 4403 // Don't know why but IE is doing this (putting http://null/ on links?! 4404 // alert(html); 4405 // var nullRE = new RegExp('https?:\/\/null\/', 'g'); 4406 // html = html.replace(nullRE, location.href.replace(/(https?:\/\/[^\/]*\/).*/, '$1')); 4407 // alert(html); 4408 // } 4409 4410 return html; 4411 }; 4412 4413 // retrieve the HTML (fastest version, but uses innerHTML) 4414 Xinha.prototype.getInnerHTML = function() 4415 { 4416 if ( !this._doc.body ) 4417 { 4418 return ''; 4419 } 4420 var html = ""; 4421 switch ( this._editMode ) 4422 { 4423 case "wysiwyg": 4424 if ( !this.config.fullPage ) 4425 { 4426 // return this._doc.body.innerHTML; 4427 html = this._doc.body.innerHTML; 4428 } 4429 else 4430 { 4431 html = this.doctype + "\n" + this._doc.documentElement.innerHTML; 4432 } 4433 break; 4434 case "textmode" : 4435 html = this._textArea.value; 4436 break; 4437 default: 4438 alert("Mode <" + this._editMode + "> not defined!"); 4439 return false; 4440 } 4441 4442 return html; 4443 }; 4444 4445 // completely change the HTML inside 4446 Xinha.prototype.setHTML = function(html) 4447 { 4448 if ( !this.config.fullPage ) 4449 { 4450 this._doc.body.innerHTML = html; 4451 } 4452 else 4453 { 4454 this.setFullHTML(html); 4455 } 4456 this._textArea.value = html; 4457 }; 4458 4459 // sets the given doctype (useful when config.fullPage is true) 4460 Xinha.prototype.setDoctype = function(doctype) 4461 { 4462 this.doctype = doctype; 4463 }; 4464 4465 /*************************************************** 4466 * Category: UTILITY FUNCTIONS 4467 ***************************************************/ 4468 4469 // variable used to pass the object to the popup editor window. 4470 Xinha._object = null; 4471 4472 // function that returns a clone of the given object 4473 Xinha.cloneObject = function(obj) 4474 { 4475 if ( !obj ) 4476 { 4477 return null; 4478 } 4479 4480 var newObj = {}; 4481 4482 // check for array objects 4483 if ( obj.constructor.toString().match( /\s*function Array\(/ ) ) 4484 { 4485 newObj = obj.constructor(); 4486 } 4487 4488 // check for function objects (as usual, IE is fucked up) 4489 if ( obj.constructor.toString().match( /\s*function Function\(/ ) ) 4490 { 4491 newObj = obj; // just copy reference to it 4492 } 4493 else 4494 { 4495 for ( var n in obj ) 4496 { 4497 var node = obj[n]; 4498 if ( typeof node == 'object' ) 4499 { 4500 newObj[n] = Xinha.cloneObject(node); 4501 } 4502 else 4503 { 4504 newObj[n] = node; 4505 } 4506 } 4507 } 4508 4509 return newObj; 4510 }; 4511 4512 // FIXME!!! this should return false for IE < 5.5 4513 Xinha.checkSupportedBrowser = function() 4514 { 4515 if ( Xinha.is_gecko ) 4516 { 4517 if ( navigator.productSub < 20021201 ) 4518 { 4519 alert("You need at least Mozilla-1.3 Alpha.\nSorry, your Gecko is not supported."); 4520 return false; 4521 } 4522 if ( navigator.productSub < 20030210 ) 4523 { 4524 alert("Mozilla < 1.3 Beta is not supported!\nI'll try, though, but it might not work."); 4525 } 4526 } 4527 return Xinha.is_gecko || Xinha.is_ie; 4528 }; 4529 4530 // selection & ranges 4531 4532 // moved Xinha.prototype._getSelection() to browser specific file 4533 // moved Xinha.prototype._createRange() to browser specific file 4534 4535 // event handling 4536 4537 /** Event Flushing 4538 * To try and work around memory leaks in the rather broken 4539 * garbage collector in IE, Xinha.flushEvents can be called 4540 * onunload, it will remove any event listeners (that were added 4541 * through _addEvent(s)) and clear any DOM-0 events. 4542 */ 4543 Xinha._eventFlushers = []; 4544 Xinha.flushEvents = function() 4545 { 4546 var x = 0; 4547 // @todo : check if Array.prototype.pop exists for every supported browsers 4548 var e = Xinha._eventFlushers.pop(); 4549 while ( e ) 4550 { 4551 try 4552 { 4553 if ( e.length == 3 ) 4554 { 4555 Xinha._removeEvent(e[0], e[1], e[2]); 4556 x++; 4557 } 4558 else if ( e.length == 2 ) 4559 { 4560 e[0]['on' + e[1]] = null; 4561 e[0]._xinha_dom0Events[e[1]] = null; 4562 x++; 4563 } 4564 } 4565 catch(ex) 4566 { 4567 // Do Nothing 4568 } 4569 e = Xinha._eventFlushers.pop(); 4570 } 4571 4572 /* 4573 // This code is very agressive, and incredibly slow in IE, so I've disabled it. 4574 4575 if(document.all) 4576 { 4577 for(var i = 0; i < document.all.length; i++) 4578 { 4579 for(var j in document.all[i]) 4580 { 4581 if(/^on/.test(j) && typeof document.all[i][j] == 'function') 4582 { 4583 document.all[i][j] = null; 4584 x++; 4585 } 4586 } 4587 } 4588 } 4589 */ 4590 4591 // alert('Flushed ' + x + ' events.'); 4592 }; 4593 4594 if ( document.addEventListener ) 4595 { 4596 Xinha._addEvent = function(el, evname, func) 4597 { 4598 el.addEventListener(evname, func, true); 4599 Xinha._eventFlushers.push([el, evname, func]); 4600 }; 4601 Xinha._removeEvent = function(el, evname, func) 4602 { 4603 el.removeEventListener(evname, func, true); 4604 }; 4605 Xinha._stopEvent = function(ev) 4606 { 4607 ev.preventDefault(); 4608 ev.stopPropagation(); 4609 }; 4610 } 4611 else if ( document.attachEvent ) 4612 { 4613 Xinha._addEvent = function(el, evname, func) 4614 { 4615 el.attachEvent("on" + evname, func); 4616 Xinha._eventFlushers.push([el, evname, func]); 4617 }; 4618 Xinha._removeEvent = function(el, evname, func) 4619 { 4620 el.detachEvent("on" + evname, func); 4621 }; 4622 Xinha._stopEvent = function(ev) 4623 { 4624 try 4625 { 4626 ev.cancelBubble = true; 4627 ev.returnValue = false; 4628 } 4629 catch (ex) 4630 { 4631 // Perhaps we could try here to stop the window.event 4632 // window.event.cancelBubble = true; 4633 // window.event.returnValue = false; 4634 } 4635 }; 4636 } 4637 else 4638 { 4639 Xinha._addEvent = function(el, evname, func) 4640 { 4641 alert('_addEvent is not supported'); 4642 }; 4643 Xinha._removeEvent = function(el, evname, func) 4644 { 4645 alert('_removeEvent is not supported'); 4646 }; 4647 Xinha._stopEvent = function(ev) 4648 { 4649 alert('_stopEvent is not supported'); 4650 }; 4651 } 4652 4653 Xinha._addEvents = function(el, evs, func) 4654 { 4655 for ( var i = evs.length; --i >= 0; ) 4656 { 4657 Xinha._addEvent(el, evs[i], func); 4658 } 4659 }; 4660 4661 Xinha._removeEvents = function(el, evs, func) 4662 { 4663 for ( var i = evs.length; --i >= 0; ) 4664 { 4665 Xinha._removeEvent(el, evs[i], func); 4666 } 4667 }; 4668 4669 /** 4670 * Adds a standard "DOM-0" event listener to an element. 4671 * The DOM-0 events are those applied directly as attributes to 4672 * an element - eg element.onclick = stuff; 4673 * 4674 * By using this function instead of simply overwriting any existing 4675 * DOM-0 event by the same name on the element it will trigger as well 4676 * as the existing ones. Handlers are triggered one after the other 4677 * in the order they are added. 4678 * 4679 * Remember to return true/false from your handler, this will determine 4680 * whether subsequent handlers will be triggered (ie that the event will 4681 * continue or be canceled). 4682 * 4683 */ 4684 4685 Xinha.addDom0Event = function(el, ev, fn) 4686 { 4687 Xinha._prepareForDom0Events(el, ev); 4688 el._xinha_dom0Events[ev].unshift(fn); 4689 }; 4690 4691 4692 /** 4693 * See addDom0Event, the difference is that handlers registered using 4694 * prependDom0Event will be triggered before existing DOM-0 events of the 4695 * same name on the same element. 4696 */ 4697 4698 Xinha.prependDom0Event = function(el, ev, fn) 4699 { 4700 Xinha._prepareForDom0Events(el, ev); 4701 el._xinha_dom0Events[ev].push(fn); 4702 }; 4703 4704 /** 4705 * Prepares an element to receive more than one DOM-0 event handler 4706 * when handlers are added via addDom0Event and prependDom0Event. 4707 */ 4708 Xinha._prepareForDom0Events = function(el, ev) 4709 { 4710 // Create a structure to hold our lists of event handlers 4711 if ( typeof el._xinha_dom0Events == 'undefined' ) 4712 { 4713 el._xinha_dom0Events = {}; 4714 Xinha.freeLater(el, '_xinha_dom0Events'); 4715 } 4716 4717 // Create a list of handlers for this event type 4718 if ( typeof el._xinha_dom0Events[ev] == 'undefined' ) 4719 { 4720 el._xinha_dom0Events[ev] = [ ]; 4721 if ( typeof el['on'+ev] == 'function' ) 4722 { 4723 el._xinha_dom0Events[ev].push(el['on'+ev]); 4724 } 4725 4726 // Make the actual event handler, which runs through 4727 // each of the handlers in the list and executes them 4728 // in the correct context. 4729 el['on'+ev] = function(event) 4730 { 4731 var a = el._xinha_dom0Events[ev]; 4732 // call previous submit methods if they were there. 4733 var allOK = true; 4734 for ( var i = a.length; --i >= 0; ) 4735 { 4736 // We want the handler to be a member of the form, not the array, so that "this" will work correctly 4737 el._xinha_tempEventHandler = a[i]; 4738 if ( el._xinha_tempEventHandler(event) === false ) 4739 { 4740 el._xinha_tempEventHandler = null; 4741 allOK = false; 4742 break; 4743 } 4744 el._xinha_tempEventHandler = null; 4745 } 4746 return allOK; 4747 }; 4748 4749 Xinha._eventFlushers.push([el, ev]); 4750 } 4751 }; 4752 4753 Xinha.prototype.notifyOn = function(ev, fn) 4754 { 4755 if ( typeof this._notifyListeners[ev] == 'undefined' ) 4756 { 4757 this._notifyListeners[ev] = []; 4758 Xinha.freeLater(this, '_notifyListeners'); 4759 } 4760 this._notifyListeners[ev].push(fn); 4761 }; 4762 4763 Xinha.prototype.notifyOf = function(ev, args) 4764 { 4765 if ( this._notifyListeners[ev] ) 4766 { 4767 for ( var i = 0; i < this._notifyListeners[ev].length; i++ ) 4768 { 4769 this._notifyListeners[ev][i](ev, args); 4770 } 4771 } 4772 }; 4773 4774 Xinha._removeClass = function(el, className) 4775 { 4776 if ( ! ( el && el.className ) ) 4777 { 4778 return; 4779 } 4780 var cls = el.className.split(" "); 4781 var ar = []; 4782 for ( var i = cls.length; i > 0; ) 4783 { 4784 if ( cls[--i] != className ) 4785 { 4786 ar[ar.length] = cls[i]; 4787 } 4788 } 4789 el.className = ar.join(" "); 4790 }; 4791 4792 Xinha._addClass = function(el, className) 4793 { 4794 // remove the class first, if already there 4795 Xinha._removeClass(el, className); 4796 el.className += " " + className; 4797 }; 4798 4799 Xinha._hasClass = function(el, className) 4800 { 4801 if ( ! ( el && el.className ) ) 4802 { 4803 return false; 4804 } 4805 var cls = el.className.split(" "); 4806 for ( var i = cls.length; i > 0; ) 4807 { 4808 if ( cls[--i] == className ) 4809 { 4810 return true; 4811 } 4812 } 4813 return false; 4814 }; 4815 4816 Xinha._blockTags = " body form textarea fieldset ul ol dl li div " + 4817 "p h1 h2 h3 h4 h5 h6 quote pre table thead " + 4818 "tbody tfoot tr td th iframe address blockquote "; 4819 Xinha.isBlockElement = function(el) 4820 { 4821 return el && el.nodeType == 1 && (Xinha._blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); 4822 }; 4823 4824 Xinha._paraContainerTags = " body td th caption fieldset div"; 4825 Xinha.isParaContainer = function(el) 4826 { 4827 return el && el.nodeType == 1 && (Xinha._paraContainerTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); 4828 }; 4829 4830 // These are all the tags for which the end tag is not optional or 4831 // forbidden, taken from the list at: 4832 // http://www.w3.org/TR/REC-html40/index/elements.html 4833 Xinha._closingTags = " a abbr acronym address applet b bdo big blockquote button caption center cite code del dfn dir div dl em fieldset font form frameset h1 h2 h3 h4 h5 h6 i iframe ins kbd label legend map menu noframes noscript object ol optgroup pre q s samp script select small span strike strong style sub sup table textarea title tt u ul var "; 4834 4835 Xinha.needsClosingTag = function(el) 4836 { 4837 return el && el.nodeType == 1 && (Xinha._closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); 4838 }; 4839 4840 // performs HTML encoding of some given string 4841 Xinha.htmlEncode = function(str) 4842 { 4843 if ( typeof str.replace == 'undefined' ) 4844 { 4845 str = str.toString(); 4846 } 4847 // we don't need regexp for that, but.. so be it for now. 4848 str = str.replace(/&/ig, "&"); 4849 str = str.replace(/</ig, "<"); 4850 str = str.replace(/>/ig, ">"); 4851 str = str.replace(/\xA0/g, " "); // Decimal 160, non-breaking-space 4852 str = str.replace(/\x22/g, """); 4853 // \x22 means '"' -- we use hex reprezentation so that we don't disturb 4854 // JS compressors (well, at least mine fails.. ;) 4855 return str; 4856 }; 4857 4858 // moved Xinha.getHTML() to getHTML.js 4859 Xinha.prototype.stripBaseURL = function(string) 4860 { 4861 if ( this.config.baseHref === null || !this.config.stripBaseHref ) 4862 { 4863 return string; 4864 } 4865 // strip host-part of URL which is added by MSIE to links relative to server root 4866 var baseurl = this.config.baseHref.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1'); 4867 var basere = new RegExp(baseurl); 4868 return string.replace(basere, ""); 4869 }; 4870 4871 String.prototype.trim = function() 4872 { 4873 return this.replace(/^\s+/, '').replace(/\s+$/, ''); 4874 }; 4875 4876 // creates a rgb-style color from a number 4877 Xinha._makeColor = function(v) 4878 { 4879 if ( typeof v != "number" ) 4880 { 4881 // already in rgb (hopefully); IE doesn't get here. 4882 return v; 4883 } 4884 // IE sends number; convert to rgb. 4885 var r = v & 0xFF; 4886 var g = (v >> 8) & 0xFF; 4887 var b = (v >> 16) & 0xFF; 4888 return "rgb(" + r + "," + g + "," + b + ")"; 4889 }; 4890 4891 // returns hexadecimal color representation from a number or a rgb-style color. 4892 Xinha._colorToRgb = function(v) 4893 { 4894 if ( !v ) 4895 { 4896 return ''; 4897 } 4898 var r,g,b; 4899 // @todo: why declaring this function here ? This needs to be a public methode of the object Xinha._colorToRgb 4900 // returns the hex representation of one byte (2 digits) 4901 function hex(d) 4902 { 4903 return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); 4904 } 4905 4906 if ( typeof v == "number" ) 4907 { 4908 // we're talking to IE here 4909 r = v & 0xFF; 4910 g = (v >> 8) & 0xFF; 4911 b = (v >> 16) & 0xFF; 4912 return "#" + hex(r) + hex(g) + hex(b); 4913 } 4914 4915 if ( v.substr(0, 3) == "rgb" ) 4916 { 4917 // in rgb(...) form -- Mozilla 4918 var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; 4919 if ( v.match(re) ) 4920 { 4921 r = parseInt(RegExp.$1, 10); 4922 g = parseInt(RegExp.$2, 10); 4923 b = parseInt(RegExp.$3, 10); 4924 return "#" + hex(r) + hex(g) + hex(b); 4925 } 4926 // doesn't match RE?! maybe uses percentages or float numbers 4927 // -- FIXME: not yet implemented. 4928 return null; 4929 } 4930 4931 if ( v.substr(0, 1) == "#" ) 4932 { 4933 // already hex rgb (hopefully :D ) 4934 return v; 4935 } 4936 4937 // if everything else fails ;) 4938 return null; 4939 }; 4940 4941 // modal dialogs for Mozilla (for IE we're using the showModalDialog() call). 4942 4943 // receives an URL to the popup dialog and a function that receives one value; 4944 // this function will get called after the dialog is closed, with the return 4945 // value of the dialog. 4946 Xinha.prototype._popupDialog = function(url, action, init) 4947 { 4948 Dialog(this.popupURL(url), action, init); 4949 }; 4950 4951 // paths 4952 4953 Xinha.prototype.imgURL = function(file, plugin) 4954 { 4955 if ( typeof plugin == "undefined" ) 4956 { 4957 return _editor_url + file; 4958 } 4959 else 4960 { 4961 return _editor_url + "plugins/" + plugin + "/img/" + file; 4962 } 4963 }; 4964 4965 Xinha.prototype.popupURL = function(file) 4966 { 4967 var url = ""; 4968 if ( file.match(/^plugin:\/\/(.*?)\/(.*)/) ) 4969 { 4970 var plugin = RegExp.$1; 4971 var popup = RegExp.$2; 4972 if ( ! ( /\.html$/.test(popup) ) ) 4973 { 4974 popup += ".html"; 4975 } 4976 url = _editor_url + "plugins/" + plugin + "/popups/" + popup; 4977 } 4978 else if ( file.match(/^\/.*?/) ) 4979 { 4980 url = file; 4981 } 4982 else 4983 { 4984 url = _editor_url + this.config.popupURL + file; 4985 } 4986 return url; 4987 }; 4988 4989 /** 4990 * FIX: Internet Explorer returns an item having the _name_ equal to the given 4991 * id, even if it's not having any id. This way it can return a different form 4992 * field even if it's not a textarea. This workarounds the problem by 4993 * specifically looking to search only elements having a certain tag name. 4994 */ 4995 Xinha.getElementById = function(tag, id) 4996 { 4997 var el, i, objs = document.getElementsByTagName(tag); 4998 for ( i = objs.length; --i >= 0 && (el = objs[i]); ) 4999 { 5000 if ( el.id == id ) 5001 { 5002 return el; 5003 } 5004 } 5005 return null; 5006 }; 5007 5008 5009 /** Use some CSS trickery to toggle borders on tables */ 5010 5011 Xinha.prototype._toggleBorders = function() 5012 { 5013 var tables = this._doc.getElementsByTagName('TABLE'); 5014 if ( tables.length !== 0 ) 5015 { 5016 if ( !this.borders ) 5017 { 5018 name = "bordered"; 5019 this.borders = true; 5020 } 5021 else 5022 { 5023 name = ""; 5024 this.borders = false; 5025 } 5026 5027 for ( var i=0; i < tables.length; i++ ) 5028 { 5029 if ( this.borders ) 5030 { 5031 // flashing the display forces moz to listen (JB:18-04-2005) - #102 5032 if ( Xinha.is_gecko ) 5033 { 5034 tables[i].style.display="none"; 5035 tables[i].style.display="table"; 5036 } 5037 Xinha._addClass(tables[i], 'htmtableborders'); 5038 } 5039 else 5040 { 5041 Xinha._removeClass(tables[i], 'htmtableborders'); 5042 } 5043 } 5044 } 5045 return true; 5046 }; 5047 5048 5049 Xinha.addClasses = function(el, classes) 5050 { 5051 if ( el !== null ) 5052 { 5053 var thiers = el.className.trim().split(' '); 5054 var ours = classes.split(' '); 5055 for ( var x = 0; x < ours.length; x++ ) 5056 { 5057 var exists = false; 5058 for ( var i = 0; exists === false && i < thiers.length; i++ ) 5059 { 5060 if ( thiers[i] == ours[x] ) 5061 { 5062 exists = true; 5063 } 5064 } 5065 if ( exists === false ) 5066 { 5067 thiers[thiers.length] = ours[x]; 5068 } 5069 } 5070 el.className = thiers.join(' ').trim(); 5071 } 5072 }; 5073 5074 Xinha.removeClasses = function(el, classes) 5075 { 5076 var existing = el.className.trim().split(); 5077 var new_classes = []; 5078 var remove = classes.trim().split(); 5079 5080 for ( var i = 0; i < existing.length; i++ ) 5081 { 5082 var found = false; 5083 for ( var x = 0; x < remove.length && !found; x++ ) 5084 { 5085 if ( existing[i] == remove[x] ) 5086 { 5087 found = true; 5088 } 5089 } 5090 if ( !found ) 5091 { 5092 new_classes[new_classes.length] = existing[i]; 5093 } 5094 } 5095 return new_classes.join(' '); 5096 }; 5097 5098 /** Alias these for convenience */ 5099 Xinha.addClass = Xinha._addClass; 5100 Xinha.removeClass = Xinha._removeClass; 5101 Xinha._addClasses = Xinha.addClasses; 5102 Xinha._removeClasses = Xinha.removeClasses; 5103 5104 /** Use XML HTTPRequest to post some data back to the server and do something 5105 * with the response (asyncronously!), this is used by such things as the tidy functions 5106 */ 5107 Xinha._postback = function(url, data, handler) 5108 { 5109 var req = null; 5110 if ( Xinha.is_ie ) 5111 { 5112 req = new ActiveXObject("Microsoft.XMLHTTP"); 5113 } 5114 else 5115 { 5116 req = new XMLHttpRequest(); 5117 } 5118 5119 var content = ''; 5120 if (typeof data == 'string') 5121 { 5122 content = data; 5123 } 5124 else if(typeof data == "object") 5125 { 5126 for ( var i in data ) 5127 { 5128 content += (content.length ? '&' : '') + i + '=' + encodeURIComponent(data[i]); 5129 } 5130 } 5131 5132 function callBack() 5133 { 5134 if ( req.readyState == 4 ) 5135 { 5136 if ( req.status == 200 || Xinha.isRunLocally && req.status == 0 ) 5137 { 5138 if ( typeof handler == 'function' ) 5139 { 5140 handler(req.responseText, req); 5141 } 5142 } 5143 else 5144 { 5145 alert('An error has occurred: ' + req.statusText); 5146 } 5147 } 5148 } 5149 5150 req.onreadystatechange = callBack; 5151 5152 req.open('POST', url, true); 5153 req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); 5154 //alert(content); 5155 req.send(content); 5156 }; 5157 5158 Xinha._getback = function(url, handler) 5159 { 5160 var req = null; 5161 if ( Xinha.is_ie ) 5162 { 5163 req = new ActiveXObject("Microsoft.XMLHTTP"); 5164 } 5165 else 5166 { 5167 req = new XMLHttpRequest(); 5168 } 5169 5170 function callBack() 5171 { 5172 if ( req.readyState == 4 ) 5173 { 5174 if ( req.status == 200 || Xinha.isRunLocally && req.status == 0 ) 5175 { 5176 handler(req.responseText, req); 5177 } 5178 else 5179 { 5180 alert('An error has occurred: ' + req.statusText); 5181 } 5182 } 5183 } 5184 5185 req.onreadystatechange = callBack; 5186 req.open('GET', url, true); 5187 req.send(null); 5188 }; 5189 5190 Xinha._geturlcontent = function(url) 5191 { 5192 var req = null; 5193 if ( Xinha.is_ie ) 5194 { 5195 req = new ActiveXObject("Microsoft.XMLHTTP"); 5196 } 5197 else 5198 { 5199 req = new XMLHttpRequest(); 5200 } 5201 5202 // Synchronous! 5203 req.open('GET', url, false); 5204 req.send(null); 5205 if ( req.status == 200 || Xinha.isRunLocally && req.status == 0 ) 5206 { 5207 return req.responseText; 5208 } 5209 else 5210 { 5211 return ''; 5212 } 5213 5214 }; 5215 5216 /** 5217 * Unless somebody already has, make a little function to debug things 5218 */ 5219 if ( typeof dump == 'undefined' ) 5220 { 5221 function dump(o) 5222 { 5223 var s = ''; 5224 for ( var prop in o ) 5225 { 5226 s += prop + ' = ' + o[prop] + '\n'; 5227 } 5228 var x = window.open("", "debugger"); 5229 x.document.write('<pre>' + s + '</pre>'); 5230 } 5231 } 5232 5233 Xinha.arrayContainsArray = function(a1, a2) 5234 { 5235 var all_found = true; 5236 for ( var x = 0; x < a2.length; x++ ) 5237 { 5238 var found = false; 5239 for ( var i = 0; i < a1.length; i++ ) 5240 { 5241 if ( a1[i] == a2[x] ) 5242 { 5243 found = true; 5244 break; 5245 } 5246 } 5247 if ( !found ) 5248 { 5249 all_found = false; 5250 break; 5251 } 5252 } 5253 return all_found; 5254 }; 5255 5256 Xinha.arrayFilter = function(a1, filterfn) 5257 { 5258 var new_a = [ ]; 5259 for ( var x = 0; x < a1.length; x++ ) 5260 { 5261 if ( filterfn(a1[x]) ) 5262 { 5263 new_a[new_a.length] = a1[x]; 5264 } 5265 } 5266 return new_a; 5267 }; 5268 5269 Xinha.uniq_count = 0; 5270 Xinha.uniq = function(prefix) 5271 { 5272 return prefix + Xinha.uniq_count++; 5273 }; 5274 5275 /** New language handling functions **/ 5276 5277 5278 /** Load a language file. 5279 * This function should not be used directly, Xinha._lc will use it when necessary. 5280 * @param context Case sensitive context name, eg 'Xinha', 'TableOperations', ... 5281 */ 5282 Xinha._loadlang = function(context) 5283 { 5284 var url, lang; 5285 if ( typeof _editor_lcbackend == "string" ) 5286 { 5287 //use backend 5288 url = _editor_lcbackend; 5289 url = url.replace(/%lang%/, _editor_lang); 5290 url = url.replace(/%context%/, context); 5291 } 5292 else 5293 { 5294 //use internal files 5295 if ( context != 'Xinha') 5296 { 5297 url = _editor_url+"plugins/"+context+"/lang/"+_editor_lang+".js"; 5298 } 5299 else 5300 { 5301 url = _editor_url+"lang/"+_editor_lang+".js"; 5302 } 5303 } 5304 5305 var langData = Xinha._geturlcontent(url); 5306 if ( langData !== "" ) 5307 { 5308 try 5309 { 5310 eval('lang = ' + langData); 5311 } 5312 catch(ex) 5313 { 5314 alert('Error reading Language-File ('+url+'):\n'+Error.toString()); 5315 lang = {}; 5316 } 5317 } 5318 else 5319 { 5320 lang = {}; 5321 } 5322 5323 return lang; 5324 }; 5325 5326 /** Return a localised string. 5327 * @param string English language string. It can also contain variables in the form "Some text with $variable=replaced text$". 5328 * This replaces $variable in "Some text with $variable" with "replaced text" 5329 * @param context Case sensitive context name, eg 'Xinha' (default), 'TableOperations'... 5330 * @param replace Replace $variables in String, eg {foo: 'replaceText'} ($foo in string will be replaced) 5331 */ 5332 Xinha._lc = function(string, context, replace) 5333 { 5334 var ret; 5335 var m = null; 5336 if (typeof string == 'string') m = string.match(/\$(.*?)=(.*?)\$/g); 5337 if (m) 5338 { 5339 if (!replace) replace = {}; 5340 for (var i = 0;i<m.length;i++) 5341 { 5342 var n = m[i].match(/\$(.*?)=(.*?)\$/); 5343 replace[n[1]] = n[2]; 5344 string = string.replace(n[0],'$'+n[1]); 5345 } 5346 } 5347 if ( _editor_lang == "en" ) 5348 { 5349 if ( typeof string == 'object' && string.string ) 5350 { 5351 ret = string.string; 5352 } 5353 else 5354 { 5355 ret = string; 5356 } 5357 } 5358 else 5359 { 5360 if ( typeof Xinha._lc_catalog == 'undefined' ) 5361 { 5362 Xinha._lc_catalog = [ ]; 5363 } 5364 5365 if ( typeof context == 'undefined' ) 5366 { 5367 context = 'Xinha'; 5368 } 5369 5370 if ( typeof Xinha._lc_catalog[context] == 'undefined' ) 5371 { 5372 Xinha._lc_catalog[context] = Xinha._loadlang(context); 5373 } 5374 5375 var key; 5376 if ( typeof string == 'object' && string.key ) 5377 { 5378 key = string.key; 5379 } 5380 else if ( typeof string == 'object' && string.string ) 5381 { 5382 key = string.string; 5383 } 5384 else 5385 { 5386 key = string; 5387 } 5388 5389 if ( typeof Xinha._lc_catalog[context][key] == 'undefined' ) 5390 { 5391 if ( context=='Xinha' ) 5392 { 5393 // Indicate it's untranslated 5394 if ( typeof string == 'object' && string.string ) 5395 { 5396 ret = string.string; 5397 } 5398 else 5399 { 5400 ret = string; 5401 } 5402 } 5403 else 5404 { 5405 //if string is not found and context is not Xinha try if it is in Xinha 5406 return Xinha._lc(string, 'Xinha', replace); 5407 } 5408 } 5409 else 5410 { 5411 ret = Xinha._lc_catalog[context][key]; 5412 } 5413 } 5414 5415 if ( typeof string == 'object' && string.replace ) 5416 { 5417 replace = string.replace; 5418 } 5419 if ( typeof replace != "undefined" ) 5420 { 5421 for ( var i in replace ) 5422 { 5423 ret = ret.replace('$'+i, replace[i]); 5424 } 5425 } 5426 5427 return ret; 5428 }; 5429 5430 Xinha.hasDisplayedChildren = function(el) 5431 { 5432 var children = el.childNodes; 5433 for ( var i = 0; i < children.length; i++ ) 5434 { 5435 if ( children[i].tagName ) 5436 { 5437 if ( children[i].style.display != 'none' ) 5438 { 5439 return true; 5440 } 5441 } 5442 } 5443 return false; 5444 }; 5445 5446 /** 5447 * Load a javascript file by inserting it in the HEAD tag and eventually call a function when loaded 5448 * @param {string} U (Url) Source url of the file to load 5449 * @param {object} C {Callback} Callback function to launch once ready (optional) 5450 * @param {object} O (scOpe) Application scope for the callback function (optional) 5451 * @param {object} B (Bonus} Arbitrary object send as a param to the callback function (optional) 5452 * @public 5453 */ 5454 Xinha._loadback = function(U, C, O, B) 5455 { 5456 var T = Xinha.is_ie ? "onreadystatechange" : "onload"; 5457 var S = document.createElement("script"); 5458 S.type = "text/javascript"; 5459 S.src = U; 5460 if ( C ) 5461 { 5462 S[T] = function() 5463 { 5464 if ( Xinha.is_ie && ! ( /loaded|complete/.test(window.event.srcElement.readyState) ) ) 5465 { 5466 return; 5467 } 5468 C.call(O ? O : this, B); 5469 S[T] = null; 5470 }; 5471 } 5472 document.getElementsByTagName("head")[0].appendChild(S); 5473 }; 5474 5475 Xinha.collectionToArray = function(collection) 5476 { 5477 var array = [ ]; 5478 for ( var i = 0; i < collection.length; i++ ) 5479 { 5480 array.push(collection.item(i)); 5481 } 5482 return array; 5483 }; 5484 5485 if ( !Array.prototype.append ) 5486 { 5487 Array.prototype.append = function(a) 5488 { 5489 for ( var i = 0; i < a.length; i++ ) 5490 { 5491 this.push(a[i]); 5492 } 5493 return this; 5494 }; 5495 } 5496 5497 Xinha.makeEditors = function(editor_names, default_config, plugin_names) 5498 { 5499 if ( typeof default_config == 'function' ) 5500 { 5501 default_config = default_config(); 5502 } 5503 5504 var editors = {}; 5505 for ( var x = 0; x < editor_names.length; x++ ) 5506 { 5507 var editor = new Xinha(editor_names[x], Xinha.cloneObject(default_config)); 5508 editor.registerPlugins(plugin_names); 5509 editors[editor_names[x]] = editor; 5510 } 5511 return editors; 5512 }; 5513 5514 Xinha.startEditors = function(editors) 5515 { 5516 for ( var i in editors ) 5517 { 5518 if ( editors[i].generate ) 5519 { 5520 editors[i].generate(); 5521 } 5522 } 5523 }; 5524 5525 Xinha.prototype.registerPlugins = function(plugin_names) 5526 { 5527 if ( plugin_names ) 5528 { 5529 for ( var i = 0; i < plugin_names.length; i++ ) 5530 { 5531 this.setLoadingMessage('Register plugin $plugin', 'Xinha', {'plugin': plugin_names[i]}); 5532 this.registerPlugin(eval(plugin_names[i])); 5533 } 5534 } 5535 }; 5536 5537 /** Utility function to base64_encode some arbitrary data, uses the builtin btoa() if it exists (Moz) */ 5538 5539 Xinha.base64_encode = function(input) 5540 { 5541 var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 5542 var output = ""; 5543 var chr1, chr2, chr3; 5544 var enc1, enc2, enc3, enc4; 5545 var i = 0; 5546 5547 do 5548 { 5549 chr1 = input.charCodeAt(i++); 5550 chr2 = input.charCodeAt(i++); 5551 chr3 = input.charCodeAt(i++); 5552 5553 enc1 = chr1 >> 2; 5554 enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 5555 enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 5556 enc4 = chr3 & 63; 5557 5558 if ( isNaN(chr2) ) 5559 { 5560 enc3 = enc4 = 64; 5561 } 5562 else if ( isNaN(chr3) ) 5563 { 5564 enc4 = 64; 5565 } 5566 5567 output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4); 5568 } while ( i < input.length ); 5569 5570 return output; 5571 }; 5572 5573 /** Utility function to base64_decode some arbitrary data, uses the builtin atob() if it exists (Moz) */ 5574 5575 Xinha.base64_decode = function(input) 5576 { 5577 var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 5578 var output = ""; 5579 var chr1, chr2, chr3; 5580 var enc1, enc2, enc3, enc4; 5581 var i = 0; 5582 5583 // remove all characters that are not A-Z, a-z, 0-9, +, /, or = 5584 input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 5585 5586 do 5587 { 5588 enc1 = keyStr.indexOf(input.charAt(i++)); 5589 enc2 = keyStr.indexOf(input.charAt(i++)); 5590 enc3 = keyStr.indexOf(input.charAt(i++)); 5591 enc4 = keyStr.indexOf(input.charAt(i++)); 5592 5593 chr1 = (enc1 << 2) | (enc2 >> 4); 5594 chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 5595 chr3 = ((enc3 & 3) << 6) | enc4; 5596 5597 output = output + String.fromCharCode(chr1); 5598 5599 if ( enc3 != 64 ) 5600 { 5601 output = output + String.fromCharCode(chr2); 5602 } 5603 if ( enc4 != 64 ) 5604 { 5605 output = output + String.fromCharCode(chr3); 5606 } 5607 } while ( i < input.length ); 5608 5609 return output; 5610 }; 5611 5612 Xinha.removeFromParent = function(el) 5613 { 5614 if ( !el.parentNode ) 5615 { 5616 return; 5617 } 5618 var pN = el.parentNode; 5619 pN.removeChild(el); 5620 return el; 5621 }; 5622 5623 Xinha.hasParentNode = function(el) 5624 { 5625 if ( el.parentNode ) 5626 { 5627 // When you remove an element from the parent in IE it makes the parent 5628 // of the element a document fragment. Moz doesn't. 5629 if ( el.parentNode.nodeType == 11 ) 5630 { 5631 return false; 5632 } 5633 return true; 5634 } 5635 5636 return false; 5637 }; 5638 5639 // moved Xinha.getOuterHTML() to browser specific file 5640 5641 // detect the size of visible area 5642 // when calling from a popup window, call Xinha.viewportSize(window) to get the size of the popup 5643 Xinha.viewportSize = function(scope) 5644 { 5645 scope = (scope) ? scope : window; 5646 var x,y; 5647 if (window.innerHeight) // all except Explorer 5648 { 5649 x = scope.innerWidth; 5650 y = scope.innerHeight; 5651 } 5652 else if (document.documentElement && document.documentElement.clientHeight) 5653 // Explorer 6 Strict Mode 5654 { 5655 x = scope.document.documentElement.clientWidth; 5656 y = scope.document.documentElement.clientHeight; 5657 } 5658 else if (document.body) // other Explorers 5659 { 5660 x = scope.document.body.clientWidth; 5661 y = scope.document.body.clientHeight; 5662 } 5663 return {'x':x,'y':y}; 5664 }; 5665 5666 // find X position of an element 5667 Xinha.findPosX = function(obj) 5668 { 5669 var curleft = 0; 5670 if ( obj.offsetParent ) 5671 { 5672 while ( obj.offsetParent ) 5673 { 5674 curleft += obj.offsetLeft; 5675 obj = obj.offsetParent; 5676 } 5677 } 5678 else if ( obj.x ) 5679 { 5680 curleft += obj.x; 5681 } 5682 return curleft; 5683 }; 5684 5685 // find Y position of an element 5686 Xinha.findPosY = function(obj) 5687 { 5688 var curtop = 0; 5689 if ( obj.offsetParent ) 5690 { 5691 while ( obj.offsetParent ) 5692 { 5693 curtop += obj.offsetTop; 5694 obj = obj.offsetParent; 5695 } 5696 } 5697 else if ( obj.y ) 5698 { 5699 curtop += obj.y; 5700 } 5701 return curtop; 5702 }; 5703 5704 Xinha.prototype.setLoadingMessage = function(string, context, replace) 5705 { 5706 if ( !this.config.showLoading || !document.getElementById("loading_sub_" + this._textArea.name) ) 5707 { 5708 return; 5709 } 5710 var elt = document.getElementById("loading_sub_" + this._textArea.name); 5711 elt.innerHTML = Xinha._lc(string, context, replace); 5712 }; 5713 5714 Xinha.prototype.removeLoadingMessage = function() 5715 { 5716 if ( !this.config.showLoading || !document.getElementById("loading_" + this._textArea.name) ) 5717 { 5718 return ; 5719 } 5720 document.body.removeChild(document.getElementById("loading_" + this._textArea.name)); 5721 }; 5722 5723 Xinha.toFree = []; 5724 Xinha.freeLater = function(obj,prop) 5725 { 5726 Xinha.toFree.push({o:obj,p:prop}); 5727 }; 5728 5729 /** 5730 * Release memory properties from object 5731 * @param {object} object The object to free memory 5732 * @param (string} prop The property to release (optional) 5733 * @private 5734 */ 5735 Xinha.free = function(obj, prop) 5736 { 5737 if ( obj && !prop ) 5738 { 5739 for ( var p in obj ) 5740 { 5741 Xinha.free(obj, p); 5742 } 5743 } 5744 else if ( obj ) 5745 { 5746 try { obj[prop] = null; } catch(x) {} 5747 } 5748 }; 5749 5750 /** IE's Garbage Collector is broken very badly. We will do our best to 5751 * do it's job for it, but we can't be perfect. 5752 */ 5753 5754 Xinha.collectGarbageForIE = function() 5755 { 5756 Xinha.flushEvents(); 5757 for ( var x = 0; x < Xinha.toFree.length; x++ ) 5758 { 5759 if ( !Xinha.toFree[x].o ) 5760 { 5761 alert("What is " + x + ' ' + Xinha.toFree[x].o); 5762 } 5763 Xinha.free(Xinha.toFree[x].o, Xinha.toFree[x].p); 5764 Xinha.toFree[x].o = null; 5765 } 5766 }; 5767 // backward compatibility 5768 HTMLArea = Xinha; 5769 Xinha.init(); 5770 Xinha.addDom0Event(window,'unload',Xinha.collectGarbageForIE); 23 document.write('<script type="text/javascript" src="'+_editor_url+'XinhaCore.js"></script>');
Note: See TracChangeset
for help on using the changeset viewer.