source: trunk/XinhaCore.js @ 999

Last change on this file since 999 was 999, checked in by ray, 11 years ago

#1195 Allow to specify an external url to load a plugin from

  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id Rev
File size: 204.1 KB
Line 
1 
2  /*--------------------------------------:noTabs=true:tabSize=2:indentSize=2:--
3    --  Xinha (is not htmlArea) - http://xinha.org
4    --
5    --  Use of Xinha is granted by the terms of the htmlArea License (based on
6    --  BSD license)  please read license.txt in this package for details.
7    --
8    --  Copyright (c) 2005-2008 Xinha Developer Team and contributors
9    -- 
10    --  Xinha was originally based on work by Mihai Bazon which is:
11    --      Copyright (c) 2003-2004 dynarch.com.
12    --      Copyright (c) 2002-2003 interactivetools.com, inc.
13    --      This copyright notice MUST stay intact for use.
14    --
15    --  Developers - Coding Style:
16    --   For the sake of not committing needlessly conflicting changes,
17    --
18    --   * New code to be indented with 2 spaces ("soft tab").
19    --   * New code preferably uses BSD-Style Bracing
20    --      if ( foo )
21    --      {
22    --        bar();
23    --      }
24    --   * Don't change brace styles.
25    --   * Don't change indentation.
26    --   * Jedit is the recommended editor, a comment of this format should be
27    --     included in the top 10 lines of the file (see the embedded edit mode)
28    --
29    --  $HeadURL:http://svn.xinha.webfactional.com/trunk/XinhaCore.js $
30    --  $LastChangedDate:2008-05-01 14:33:36 +0200 (Do, 01 Mai 2008) $
31    --  $LastChangedRevision:998M $
32    --  $LastChangedBy:(local) $
33    --------------------------------------------------------------------------*/
34
35Xinha.version =
36{
37  'Release'   : 'Trunk',
38  'Head'      : '$HeadURL:http://svn.xinha.webfactional.com/trunk/XinhaCore.js $'.replace(/^[^:]*: (.*) \$$/, '$1'),
39  'Date'      : '$LastChangedDate:2008-05-01 14:33:36 +0200 (Do, 01 Mai 2008) $'.replace(/^[^:]*: ([0-9-]*) ([0-9:]*) ([+0-9]*) \((.*)\) \$/, '$4 $2 $3'),
40  'Revision'  : '$LastChangedRevision:998M $'.replace(/^[^:]*: (.*) \$$/, '$1'),
41  'RevisionBy': '$LastChangedBy:(local) $'.replace(/^[^:]*: (.*) \$$/, '$1')
42};
43
44//must be here. it is called while converting _editor_url to absolute
45Xinha._resolveRelativeUrl = function( base, url )
46{
47  if(url.match(/^([^:]+\:)?\/\//))
48  {
49    return url;
50  }
51  else
52  {
53    var b = base.split("/");
54    if(b[b.length - 1] == "")
55    {
56      b.pop();
57    }
58    var p = url.split("/");
59    if(p[0] == ".")
60    {
61      p.shift();
62    }
63    while(p[0] == "..")
64    {
65      b.pop();
66      p.shift();
67    }
68    return b.join("/") + "/" + p.join("/");
69  }
70}
71
72if ( typeof _editor_url == "string" )
73{
74  // Leave exactly one backslash at the end of _editor_url
75  _editor_url = _editor_url.replace(/\x2f*$/, '/');
76 
77  // convert _editor_url to absolute
78  if(!_editor_url.match(/^([^:]+\:)?\//)){
79    var path = window.location.toString().split("/");
80    path.pop();
81    _editor_url = Xinha._resolveRelativeUrl(path.join("/"), _editor_url);
82  }
83}
84else
85{
86  alert("WARNING: _editor_url is not set!  You should set this variable to the editor files path; it should preferably be an absolute path, like in '/htmlarea/', but it can be relative if you prefer.  Further we will try to load the editor files correctly but we'll probably fail.");
87  _editor_url = '';
88}
89
90// make sure we have a language
91if ( typeof _editor_lang == "string" )
92{
93  _editor_lang = _editor_lang.toLowerCase();
94}
95else
96{
97  _editor_lang = "en";
98}
99
100// skin stylesheet to load
101if ( typeof _editor_skin !== "string" )
102{
103  _editor_skin = "";
104}
105/**
106* The list of Xinha editors on the page. May be multiple editors.
107* You can access each editor object through this global variable.
108*
109* Example:<br />
110* <code>
111*       var html = __xinhas[0].getEditorContent(); // gives you the HTML of the first editor in the page
112* </code>
113*/
114var __xinhas = [];
115
116// browser identification
117/** Cache the user agent for the following checks
118 * @private
119 */
120Xinha.agt       = navigator.userAgent.toLowerCase();
121/** Browser is Microsoft Internet Explorer
122@type string
123*/
124Xinha.is_ie    = ((Xinha.agt.indexOf("msie") != -1) && (Xinha.agt.indexOf("opera") == -1));
125/** Version Number, if browser is Microsoft Internet Explorer
126@type string
127*/
128Xinha.ie_version= parseFloat(Xinha.agt.substring(Xinha.agt.indexOf("msie")+5));
129/** Browser is Opera
130@type string
131*/
132Xinha.is_opera  = (Xinha.agt.indexOf("opera") != -1);
133/** Version Number, if browser is Opera
134@type string
135*/
136if(Xinha.is_opera && Xinha.agt.match(/opera[\/ ]([0-9.]+)/))
137{
138  Xinha.opera_version = parseFloat(RegExp.$1);
139}
140else
141{
142  Xinha.opera_version = 0;
143}
144/** Browserengine is KHTML (Konqueror, Safari)
145@type string
146*/
147Xinha.is_khtml  = (Xinha.agt.indexOf("khtml") != -1);
148/** Browser is WebKit
149@type string
150*/
151Xinha.is_webkit  = (Xinha.agt.indexOf("applewebkit") != -1);
152Xinha.webkit_version = parseInt(navigator.appVersion.replace(/.*?AppleWebKit\/([\d]).*?/,'$1'));
153
154/** Browser is Safari
155@type string
156*/
157Xinha.is_safari  = (Xinha.agt.indexOf("safari") != -1);
158/** OS is MacOS
159@type string
160*/
161Xinha.is_mac       = (Xinha.agt.indexOf("mac") != -1);
162/** Browser is Microsoft Internet Explorer Mac
163@type string
164*/
165Xinha.is_mac_ie = (Xinha.is_ie && Xinha.is_mac);
166/** Browser is Microsoft Internet Explorer Windows
167@type string
168*/
169Xinha.is_win_ie = (Xinha.is_ie && !Xinha.is_mac);
170/** Browser engine is Gecko (Mozilla), applies also to Safari and Opera which work
171 *  largely similar.
172@type string
173*/
174Xinha.is_gecko  = (navigator.product == "Gecko") || Xinha.is_opera;
175Xinha.is_real_gecko = (navigator.product == "Gecko" && !Xinha.is_webkit);
176Xinha.is_ff3 = Xinha.is_real_gecko && parseInt(navigator.productSub) >= 2007121016;
177Xinha.is_ff2 = Xinha.is_real_gecko && parseInt(navigator.productSub) < 2007121016;
178
179/** File is opened locally opened ("file://" protocol)
180 * @type string
181 * @private
182 */
183Xinha.isRunLocally = document.URL.toLowerCase().search(/^file:/) != -1;
184/** Editing is enabled by document.designMode (Gecko, Opera), as opposed to contenteditable (IE)
185 * @type string
186 * @private
187 */
188Xinha.is_designMode = (typeof document.designMode != 'undefined' && !Xinha.is_ie); // IE has designMode, but we're not using it
189
190/** Check if Xinha can run in the used browser, otherwise the textarea will be remain unchanged
191 * @type Boolean
192 * @private
193 */
194Xinha.checkSupportedBrowser = function()
195{
196  return Xinha.is_real_gecko || (Xinha.is_opera && Xinha.opera_version >= 9.2) || Xinha.ie_version >= 5.5 || Xinha.webkit_version >= 522;
197};
198/** Cache result of checking for browser support
199 * @type Boolean
200 * @private
201 */
202Xinha.isSupportedBrowser = Xinha.checkSupportedBrowser();
203
204if ( Xinha.isRunLocally && Xinha.isSupportedBrowser)
205{
206  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.');
207}
208
209/** Creates a new Xinha object
210 * @version $Rev:998M $ $LastChangedDate:2008-05-01 14:33:36 +0200 (Do, 01 Mai 2008) $
211 * @constructor
212 * @param {String|DomNode}   textarea the textarea to replace; can be either only the id or the DOM object as returned by document.getElementById()
213 * @param {Xinha.Config} config optional if no Xinha.Config object is passed, the default config is used
214 */
215function Xinha(textarea, config)
216{
217  if ( !Xinha.isSupportedBrowser ) return;
218 
219  if ( !textarea )
220  {
221    throw new Error ("Tried to create Xinha without textarea specified.");
222  }
223
224  if ( typeof config == "undefined" )
225  {
226                /** The configuration used in the editor
227                 * @type Xinha.Config
228                 */
229    this.config = new Xinha.Config();
230  }
231  else
232  {
233    this.config = config;
234  }
235
236  if ( typeof textarea != 'object' )
237  {
238    textarea = Xinha.getElementById('textarea', textarea);
239  }
240  /** This property references the original textarea, which is at the same time the editor in text mode
241   * @type DomNode textarea
242   */
243  this._textArea = textarea;
244  this._textArea.spellcheck = false;
245  Xinha.freeLater(this, '_textArea');
246 
247  //
248  /** Before we modify anything, get the initial textarea size
249   * @private
250   * @type Object w,h
251   */
252  this._initial_ta_size =
253  {
254    w: textarea.style.width  ? textarea.style.width  : ( textarea.offsetWidth  ? ( textarea.offsetWidth  + 'px' ) : ( textarea.cols + 'em') ),
255    h: textarea.style.height ? textarea.style.height : ( textarea.offsetHeight ? ( textarea.offsetHeight + 'px' ) : ( textarea.rows + 'em') )
256  };
257
258  if ( document.getElementById("loading_" + textarea.id) || this.config.showLoading )
259  {
260    if (!document.getElementById("loading_" + textarea.id))
261    {
262      Xinha.createLoadingMessage(textarea);
263    }
264    this.setLoadingMessage(Xinha._lc("Constructing object"));
265  }
266
267  /** the current editing mode
268  * @private
269  * @type string "wysiwyg"|"text"
270  */
271  this._editMode = "wysiwyg";
272  /** this object holds the plugins used in the editor
273  * @private
274  * @type Object
275  */
276  this.plugins = {};
277  /** periodically updates the toolbar
278  * @private
279  * @type timeout
280  */
281  this._timerToolbar = null;
282  /** periodically takes a snapshot of the current editor content
283  * @private
284  * @type timeout
285  */
286  this._timerUndo = null;
287  /** holds the undo snapshots
288  * @private
289  * @type Array
290  */
291  this._undoQueue = [this.config.undoSteps];
292  /** the current position in the undo queue
293  * @private
294  * @type integer
295  */
296  this._undoPos = -1;
297  /** use our own undo implementation (true) or the browser's (false)
298  * @private
299  * @type Boolean
300  */
301  this._customUndo = true;
302  /** the document object of the page Xinha is embedded in
303  * @private
304  * @type document
305  */
306  this._mdoc = document; // cache the document, we need it in plugins
307  /** doctype of the edited document (fullpage mode)
308  * @private
309  * @type string
310  */
311  this.doctype = '';
312  /** running number that identifies the current editor
313  * @public
314  * @type integer
315  */
316  this.__htmlarea_id_num = __xinhas.length;
317  __xinhas[this.__htmlarea_id_num] = this;
318       
319  /** holds the events for use with the notifyOn/notifyOf system
320  * @private
321  * @type Object
322  */
323  this._notifyListeners = {};
324
325  // Panels
326  var panels =
327  {
328    right:
329    {
330      on: true,
331      container: document.createElement('td'),
332      panels: []
333    },
334    left:
335    {
336      on: true,
337      container: document.createElement('td'),
338      panels: []
339    },
340    top:
341    {
342      on: true,
343      container: document.createElement('td'),
344      panels: []
345    },
346    bottom:
347    {
348      on: true,
349      container: document.createElement('td'),
350      panels: []
351    }
352  };
353
354  for ( var i in panels )
355  {
356    if(!panels[i].container) { continue; } // prevent iterating over wrong type
357    panels[i].div = panels[i].container; // legacy
358    panels[i].container.className = 'panels ' + i;
359    Xinha.freeLater(panels[i], 'container');
360    Xinha.freeLater(panels[i], 'div');
361  }
362  /** holds the panels
363  * @private
364  * @type Array
365  */
366  // finally store the variable
367  this._panels = panels;
368       
369  // Init some properties that are defined later
370  /** The statusbar container
371   * @type DomNode statusbar div
372   */
373  this._statusBar = null;
374  /** The DOM path that is shown in the statusbar in wysiwyg mode
375   * @private
376   * @type DomNode
377   */
378  this._statusBarTree = null;
379  /** The message that is shown in the statusbar in text mode
380   * @private
381   * @type DomNode
382   */
383  this._statusBarTextMode = null;
384  /** Holds the items of the DOM path that is shown in the statusbar in wysiwyg mode
385   * @private
386   * @type Array tag names
387   */
388  this._statusBarItems = [];
389  /** Holds the parts (table cells) of the UI (toolbar, panels, statusbar)
390
391   * @type Object framework parts
392   */
393  this._framework = {};
394  /** Them whole thing (table)
395   * @private
396   * @type DomNode
397   */
398  this._htmlArea = null;
399  /** This is the actual editable area.<br />
400   *  Technically it's an iframe that's made editable using window.designMode = 'on', respectively document.body.contentEditable = true (IE).<br />
401   *  Use this property to get a grip on the iframe's window features<br />
402   *
403   * @type window
404   */
405  this._iframe = null;
406  /** The document object of the iframe.<br />
407  *   Use this property to perform DOM operations on the edited document
408  * @type document
409  */
410  this._doc = null;
411  /** The toolbar
412   *  @private
413   *  @type DomNode
414   */
415  this._toolBar = this._toolbar = null; //._toolbar is for legacy, ._toolBar is better thanks.
416  /** Holds the botton objects
417   *  @private
418   *  @type Object
419   */
420  this._toolbarObjects = {};
421 
422}
423
424Xinha.onload = function() { };
425Xinha.init = function() { Xinha.onload(); };
426
427// cache some regexps
428/** Identifies HTML tag names
429* @type RegExp
430*/
431Xinha.RE_tagName  = /(<\/|<)\s*([^ \t\n>]+)/ig;
432/** Exracts DOCTYPE string from HTML
433* @type RegExp
434*/
435Xinha.RE_doctype  = /(<!doctype((.|\n)*?)>)\n?/i;
436/** Finds head section in HTML
437* @type RegExp
438*/
439Xinha.RE_head     = /<head>((.|\n)*?)<\/head>/i;
440/** Finds body section in HTML
441* @type RegExp
442*/
443Xinha.RE_body     = /<body[^>]*>((.|\n|\r|\t)*?)<\/body>/i;
444/** Special characters that need to be escaped when dynamically creating a RegExp from an arbtrary string
445* @private
446* @type RegExp
447*/
448Xinha.RE_Specials = /([\/\^$*+?.()|{}[\]])/g;
449/** When dynamically creating a RegExp from an arbtrary string, some charactes that have special meanings in regular expressions have to be escaped.
450*   Run any string through this function to escape reserved characters.
451* @param {string} string the string to be escaped
452* @returns string
453*/
454Xinha.escapeStringForRegExp = function (string)
455{
456  return string.replace(Xinha.RE_Specials, '\\$1');
457}
458/** Identifies email addresses
459* @type RegExp
460*/
461Xinha.RE_email    = /^[_a-z\d\-\.]{3,}@[_a-z\d\-]{2,}(\.[_a-z\d\-]{2,})+$/i;
462/** Identifies URLs
463* @type RegExp
464*/
465Xinha.RE_url      = /(https?:\/\/)?(([a-z0-9_]+:[a-z0-9_]+@)?[a-z0-9_-]{2,}(\.[a-z0-9_-]{2,}){2,}(:[0-9]+)?(\/\S+)*)/i;
466
467
468
469/**
470 * This class creates an object that can be passed to the Xinha constructor as a parameter.
471 * Set the object's properties as you need to configure the editor (toolbar etc.)
472 * @version $Rev:998M $ $LastChangedDate:2008-05-01 14:33:36 +0200 (Do, 01 Mai 2008) $
473 * @constructor
474 */
475Xinha.Config = function()
476{
477  this.version = Xinha.version.Revision;
478 
479 /** This property controls the width of the editor.<br />
480  *  Allowed values are 'auto', 'toolbar' or a numeric value followed by "px".<br />
481  *  <code>auto</code>: let Xinha choose the width to use.<br />
482  *  <code>toolbar</code>: compute the width size from the toolbar width.<br />
483  *  <code>numeric value</code>: forced width in pixels ('600px').<br />
484  *
485  *  Default: <code>"auto"</code>
486  * @type String
487  */
488  this.width  = "auto";
489 /** This property controls the height of the editor.<br />
490  *  Allowed values are 'auto' or a numeric value followed by px.<br />
491  *  <code>"auto"</code>: let Xinha choose the height to use.<br />
492  *  <code>numeric value</code>: forced height in pixels ('200px').<br />
493  *  Default: <code>"auto"</code>
494  * @type String
495  */
496  this.height = "auto";
497
498 /** Specifies whether the toolbar should be included
499  *  in the size, or are extra to it.  If false then it's recommended
500  *  to have the size set as explicit pixel sizes (either in Xinha.Config or on your textarea)<br />
501  *
502  *  Default: <code>true</code>
503  *
504  *  @type Boolean
505  */
506  this.sizeIncludesBars = true;
507 /**
508  * Specifies whether the panels should be included
509  * in the size, or are extra to it.  If false then it's recommended
510  * to have the size set as explicit pixel sizes (either in Xinha.Config or on your textarea)<br />
511  * 
512  *  Default: <code>true</code>
513  *
514  *  @type Boolean
515  */
516  this.sizeIncludesPanels = true;
517
518 /**
519  * each of the panels has a dimension, for the left/right it's the width
520  * for the top/bottom it's the height.
521  *
522  * WARNING: PANEL DIMENSIONS MUST BE SPECIFIED AS PIXEL WIDTHS<br />
523  *Default values: 
524  *<pre>
525  *       xinha_config.panel_dimensions =
526  *   {
527  *         left:   '200px', // Width
528  *         right:  '200px',
529  *         top:    '100px', // Height
530  *         bottom: '100px'
531  *       }
532  *</pre>
533  *  @type Object
534  */
535  this.panel_dimensions =
536  {
537    left:   '200px', // Width
538    right:  '200px',
539    top:    '100px', // Height
540    bottom: '100px'
541  };
542
543 /**  To make the iframe width narrower than the toolbar width, e.g. to maintain
544  *   the layout when editing a narrow column of text, set the next parameter (in pixels).<br />
545  *
546  *  Default: <code>true</code>
547  *
548  *  @type Integer|null
549  */
550  this.iframeWidth = null;
551 
552 /** Enable creation of the status bar?<br />
553  *
554  *  Default: <code>true</code>
555  *
556  *  @type Boolean
557  */
558  this.statusBar = true;
559
560 /** Intercept ^V and use the Xinha paste command
561  *  If false, then passes ^V through to browser editor widget, which is the only way it works without problems in Mozilla<br />
562  *
563  *  Default: <code>false</code>
564  *
565  *  @type Boolean
566  */
567  this.htmlareaPaste = false;
568 
569 /** <strong>Gecko only:</strong> Let the built-in routine for handling the <em>return</em> key decide if to enter <em>br</em> or <em>p</em> tags,
570  *  or use a custom implementation.<br />
571  *  For information about the rules applied by Gecko, <a href="http://www.mozilla.org/editor/rules.html">see Mozilla website</a> <br />
572  *  Possible values are <em>built-in</em> or <em>best</em><br />
573  *
574  *  Default: <code>"best"</code>
575  *
576  *  @type String
577  */
578  this.mozParaHandler = 'best';
579 
580 /** This determines the method how the HTML output is generated.
581  *  There are two choices:
582  *
583  *<table border="1">
584  *   <tr>
585  *       <td><em>DOMwalk</em></td>
586  *       <td>This is the classic and proven method. It recusively traverses the DOM tree
587  *           and builds the HTML string "from scratch". Tends to be a bit slow, especially in IE.</td>
588  *   </tr>
589  *   <tr>
590  *       <td><em>TransformInnerHTML</em></td>
591  *       <td>This method uses the JavaScript innerHTML property and relies on Regular Expressions to produce
592  *            clean XHTML output. This method is much faster than the other one.</td>
593  *     </tr>
594  * </table>
595  *
596  *  Default: <code>"DOMwalk"</code>
597  *
598  * @type String
599  */
600  this.getHtmlMethod = 'DOMwalk';
601 
602  /** Maximum size of the undo queue<br />
603   *  Default: <code>20</code>
604   *  @type Integer
605   */
606  this.undoSteps = 20;
607
608  /** The time interval at which undo samples are taken<br />
609   *  Default: <code>500</code> (1/2 sec)
610   *  @type Integer milliseconds
611   */
612  this.undoTimeout = 500;
613
614  /** Set this to true if you want to explicitly right-justify when setting the text direction to right-to-left<br />
615   *  Default: <code>false</code>
616   *  @type Boolean
617   */
618  this.changeJustifyWithDirection = false;
619
620  /** If true then Xinha will retrieve the full HTML, starting with the &lt;HTML&gt; tag.<br />
621   *  Default: <code>false</code>
622   *  @type Boolean
623   */
624  this.fullPage = false;
625
626  /** Raw style definitions included in the edited document<br />
627   *  When a lot of inline style is used, perhaps it is wiser to use one or more external stylesheets.<br />
628   *  To set tags P in red, H1 in blue andn A not underlined, we may do the following
629   *<pre>
630   * xinha_config.pageStyle =
631   *  'p { color:red; }\n' +
632   *  'h1 { color:bleu; }\n' +
633   *  'a {text-decoration:none; }';
634   *</pre>
635   *  Default: <code>""</code> (empty)
636   *  @type String
637   */
638  this.pageStyle = "";
639
640  /** Array of external stylesheets to load. (Reference these absolutely)<br />
641   *  Example<br />
642   *  <pre>xinha_config.pageStyleSheets = ["/css/myPagesStyleSheet.css","/css/anotherOne.css"];</pre>
643   *  Default: <code>[]</code> (empty)
644   *  @type Array
645   */
646  this.pageStyleSheets = [];
647
648  // specify a base href for relative links
649  /** Specify a base href for relative links<br />
650   *  ATTENTION: this does not work as expected and needs t be changed, see Ticket #961 <br />
651   *  Default: <code>null</code>
652   *  @type String|null
653   */
654  this.baseHref = null;
655
656  /** If true, relative URLs (../) will be made absolute.
657   *  When the editor is in different directory depth
658   *  as the edited page relative image sources will break the display of your images.
659   *  this fixes an issue where Mozilla converts the urls of images and links that are on the same server
660   *  to relative ones (../) when dragging them around in the editor (Ticket #448)<br />
661   *  Default: <code>true</code>
662   *  @type Boolean
663   */
664  this.expandRelativeUrl = true;
665 
666 /**  We can strip the server part out of URL to make/leave them semi-absolute, reason for this
667   *  is that the browsers will prefix  the server to any relative links to make them absolute,
668   *  which isn't what you want most the time.<br />
669   *  Default: <code>true</code>
670   *  @type Boolean
671   */
672  this.stripBaseHref = true;
673
674   /**  We can strip the url of the editor page from named links (eg &lt;a href="#top"&gt;...&lt;/a&gt;) and links
675   *  that consist only of URL parameters (eg &lt;a href="?parameter=value"&gt;...&lt;/a&gt;)
676   *  reason for this is that browsers tend to prefixe location.href to any href that
677   *  that don't have a full url<br />
678   *  Default: <code>true</code>
679   *  @type Boolean
680   */
681  this.stripSelfNamedAnchors = true;
682
683  /** In URLs all characters above ASCII value 127 have to be encoded using % codes<br />
684   *  Default: <code>true</code>
685   *  @type Boolean
686   */
687  this.only7BitPrintablesInURLs = true;
688
689 
690  /** If you are putting the HTML written in Xinha into an email you might want it to be 7-bit
691   *  characters only.  This config option will convert all characters consuming
692   *  more than 7bits into UNICODE decimal entity references (actually it will convert anything
693   *  below <space> (chr 20) except cr, lf and tab and above <tilde> (~, chr 7E))<br />
694   *  Default: <code>false</code>
695   *  @type Boolean
696   */
697  this.sevenBitClean = false;
698
699
700  /** Sometimes we want to be able to replace some string in the html coming in and going out
701   *  so that in the editor we use the "internal" string, and outside and in the source view
702   *  we use the "external" string  this is useful for say making special codes for
703   *  your absolute links, your external string might be some special code, say "{server_url}"
704   *  an you say that the internal represenattion of that should be http://your.server/<br />
705   *  Example:  <code>{ 'html_string' : 'wysiwyg_string' }</code><br />
706   *  Default: <code>{}</code> (empty)
707   *  @type Object
708   */
709  this.specialReplacements = {}; //{ 'html_string' : 'wysiwyg_string' }
710 
711  /** A filter function for the HTML used inside the editor<br />
712   * Default: function (html) { return html }
713   *
714   * @param {String} html The whole document's HTML content
715   * @return {String} The processed HTML
716   */
717  this.inwardHtml = function (html) { return html }
718 
719  /** A filter function for the generated HTML<br />
720   * Default: function (html) { return html }
721   *
722   * @param {String} html The whole document's HTML content
723   * @return {String} The processed HTML
724   */
725  this.outwardHtml = function (html) { return html }
726
727 /** Set to true if you want Word code to be cleaned upon Paste. This only works if
728   * you use the toolbr button to paste, not ^V. This means that due to the restrictions
729   * regarding pasting, this actually has no real effect in Mozilla <br />
730   *  Default: <code>true</code>
731   *  @type Boolean
732   */
733  this.killWordOnPaste = true;
734
735  /** Enable the 'Target' field in the Make Link dialog. Note that the target attribute is invalid in (X)HTML strict<br />
736   *  Default: <code>true</code>
737   *  @type Boolean
738   */
739  this.makeLinkShowsTarget = true;
740
741  /** CharSet of the iframe, default is the charset of the document
742   *  @type String
743   */
744  this.charSet = (typeof document.characterSet != 'undefined') ? document.characterSet : document.charset;
745
746 /** Whether the edited document should be rendered in Quirksmode or Standard Compliant (Strict) Mode.<br />
747   * This is commonly known as the "doctype switch"<br />
748   * for details read here http://www.quirksmode.org/css/quirksmode.html
749   *
750   * Possible values:<br />
751   *    true     :  Quirksmode is used<br />
752   *    false    :  Strict mode is used<br />
753   *    null (default):  the mode of the document Xinha is in is used
754   * @type Boolean|null
755   */
756  this.browserQuirksMode = null;
757
758  // URL-s
759  this.imgURL = "images/";
760  this.popupURL = "popups/";
761
762  /** RegExp allowing to remove certain HTML tags when rendering the HTML.<br />
763   *  Example: remove span and font tags
764   *  <code>
765   *    xinha_config.htmlRemoveTags = /span|font/;
766   *  </code>
767   *  Default: <code>null</code>
768   *  @type RegExp|null
769   */
770  this.htmlRemoveTags = null;
771
772 /** Turning this on will turn all "linebreak" and "separator" items in your toolbar into soft-breaks,
773   * this means that if the items between that item and the next linebreak/separator can
774   * fit on the same line as that which came before then they will, otherwise they will
775   * float down to the next line.
776
777   * If you put a linebreak and separator next to each other, only the separator will
778   * take effect, this allows you to have one toolbar that works for both flowToolbars = true and false
779   * infact the toolbar below has been designed in this way, if flowToolbars is false then it will
780   * create explictly two lines (plus any others made by plugins) breaking at justifyleft, however if
781   * flowToolbars is false and your window is narrow enough then it will create more than one line
782   * even neater, if you resize the window the toolbars will reflow.  <br />
783   *  Default: <code>true</code>
784   *  @type Boolean
785   */
786  this.flowToolbars = true;
787 
788  /** Set to center or right to change button alignment in toolbar
789   *  @type String
790   */
791  this.toolbarAlign = "left";
792 
793  /** Set to true if you want the loading panel to show at startup<br />
794   *  Default: <code>false</code>
795   *  @type Boolean
796   */
797  this.showLoading = false;
798 
799  /** Set to false if you want to allow JavaScript in the content, otherwise &lt;script&gt; tags are stripped out.<br />
800   *  This currently only affects the "DOMwalk" getHtmlMethod.<br />
801   *  Default: <code>true</code>
802   *  @type Boolean
803   */
804  this.stripScripts = true;
805
806 /** See if the text just typed looks like a URL, or email address
807   * and link it appropriatly
808   * Note: Setting this option to false only affects Mozilla based browsers.
809   * In InternetExplorer this is native behaviour and cannot be turned off.<br />
810   *  Default: <code>true</code>
811   *  @type Boolean
812   */
813   this.convertUrlsToLinks = true;
814
815
816 /** Size of color picker cells<br />
817   * Use number + "px"<br />
818   *  Default: <code>"6px"</code>
819   *  @type String
820   */
821  this.colorPickerCellSize = '6px';
822 /** Granularity of color picker cells (number per column/row)<br />
823   *  Default: <code>18</code>
824   *  @type Integer
825   */
826  this.colorPickerGranularity = 18;
827 /** Position of color picker from toolbar button<br />
828   *  Default: <code>"bottom,right"</code>
829   *  @type String
830   */
831  this.colorPickerPosition = 'bottom,right';
832  /** Set to true to show only websafe checkbox in picker<br />
833   *  Default: <code>false</code>
834   *  @type Boolean
835   */
836  this.colorPickerWebSafe = false;
837 /** Number of recent colors to remember<br />
838   *  Default: <code>20</code>
839   *  @type Integer
840   */
841  this.colorPickerSaveColors = 20;
842
843  /** Start up the editor in fullscreen mode<br />
844   *  Default: <code>false</code>
845   *  @type Boolean
846   */
847  this.fullScreen = false;
848 
849 /** You can tell the fullscreen mode to leave certain margins on each side.<br />
850   *  The value is an array with the values for <code>[top,right,bottom,left]</code> in that order<br />
851   *  Default: <code>[0,0,0,0]</code>
852   *  @type Array
853   */
854  this.fullScreenMargins = [0,0,0,0];
855 
856  /** This array orders all buttons except plugin buttons in the toolbar. Plugin buttons typically look for one
857   *  a certain button in the toolbar and place themselves next to it.
858   * Default value:
859   *<pre>
860   *xinha_config.toolbar =
861   * [
862   *   ["popupeditor"],
863   *   ["separator","formatblock","fontname","fontsize","bold","italic","underline","strikethrough"],
864   *   ["separator","forecolor","hilitecolor","textindicator"],
865   *   ["separator","subscript","superscript"],
866   *   ["linebreak","separator","justifyleft","justifycenter","justifyright","justifyfull"],
867   *   ["separator","insertorderedlist","insertunorderedlist","outdent","indent"],
868   *   ["separator","inserthorizontalrule","createlink","insertimage","inserttable"],
869   *   ["linebreak","separator","undo","redo","selectall","print"], (Xinha.is_gecko ? [] : ["cut","copy","paste","overwrite","saveas"]),
870   *   ["separator","killword","clearfonts","removeformat","toggleborders","splitblock","lefttoright", "righttoleft"],
871   *   ["separator","htmlmode","showhelp","about"]
872   * ];
873   *</pre>
874   * @type Array
875   */ 
876  this.toolbar =
877  [
878    ["popupeditor"],
879    ["separator","formatblock","fontname","fontsize","bold","italic","underline","strikethrough"],
880    ["separator","forecolor","hilitecolor","textindicator"],
881    ["separator","subscript","superscript"],
882    ["linebreak","separator","justifyleft","justifycenter","justifyright","justifyfull"],
883    ["separator","insertorderedlist","insertunorderedlist","outdent","indent"],
884    ["separator","inserthorizontalrule","createlink","insertimage","inserttable"],
885    ["linebreak","separator","undo","redo","selectall","print"], (Xinha.is_gecko ? [] : ["cut","copy","paste","overwrite","saveas"]),
886    ["separator","killword","clearfonts","removeformat","toggleborders","splitblock","lefttoright", "righttoleft"],
887    ["separator","htmlmode","showhelp","about"]
888  ];
889
890  /** The fontnames listed in the fontname dropdown
891   * Default value:
892   *<pre>
893   *xinha_config.fontname =
894   *{
895   *  "&mdash; font &mdash;" : '',
896   *  "Arial"                : 'arial,helvetica,sans-serif',
897   *  "Courier New"          : 'courier new,courier,monospace',
898   *  "Georgia"              : 'georgia,times new roman,times,serif',
899   *  "Tahoma"               : 'tahoma,arial,helvetica,sans-serif',
900   *  "Times New Roman"      : 'times new roman,times,serif',
901   *  "Verdana"              : 'verdana,arial,helvetica,sans-serif',
902   *  "impact"               : 'impact',
903   *  "WingDings"            : 'wingdings'
904   *};
905   *</pre>
906   * @type Object
907   */
908  this.fontname =
909  {
910    "&mdash; font &mdash;": '',
911    "Arial"           : 'arial,helvetica,sans-serif',
912    "Courier New"     : 'courier new,courier,monospace',
913    "Georgia"         : 'georgia,times new roman,times,serif',
914    "Tahoma"          : 'tahoma,arial,helvetica,sans-serif',
915    "Times New Roman" : 'times new roman,times,serif',
916    "Verdana"         : 'verdana,arial,helvetica,sans-serif',
917    "impact"          : 'impact',
918    "WingDings"       : 'wingdings'
919  };
920
921  /** The fontsizes listed in the fontsize dropdown
922   * Default value:
923   *<pre>
924   *xinha_config.fontsize =
925   *{
926   *  "&mdash; size &mdash;": "",
927   *  "1 (8 pt)" : "1",
928   *  "2 (10 pt)": "2",
929   *  "3 (12 pt)": "3",
930   *  "4 (14 pt)": "4",
931   *  "5 (18 pt)": "5",
932   *  "6 (24 pt)": "6",
933   *  "7 (36 pt)": "7"
934   *};
935   *</pre>
936   * @type Object
937   */
938  this.fontsize =
939  {
940    "&mdash; size &mdash;": "",
941    "1 (8 pt)" : "1",
942    "2 (10 pt)": "2",
943    "3 (12 pt)": "3",
944    "4 (14 pt)": "4",
945    "5 (18 pt)": "5",
946    "6 (24 pt)": "6",
947    "7 (36 pt)": "7"
948  };
949  /** The tags listed in the formatblock dropdown
950   * Default value:
951   *<pre>
952   *xinha_config.formatblock =
953   *{
954   *  "&mdash; size &mdash;": "",
955   *  "1 (8 pt)" : "1",
956   *  "2 (10 pt)": "2",
957   *  "3 (12 pt)": "3",
958   *  "4 (14 pt)": "4",
959   *  "5 (18 pt)": "5",
960   *  "6 (24 pt)": "6",
961   *  "7 (36 pt)": "7"
962   *};
963   *</pre>
964   * @type Object
965   */
966  this.formatblock =
967  {
968    "&mdash; format &mdash;": "",
969    "Heading 1": "h1",
970    "Heading 2": "h2",
971    "Heading 3": "h3",
972    "Heading 4": "h4",
973    "Heading 5": "h5",
974    "Heading 6": "h6",
975    "Normal"   : "p",
976    "Address"  : "address",
977    "Formatted": "pre"
978  };
979  /** ??
980   * Default: <code>{}</code>
981   * @type Object
982   */
983  this.customSelects = {};
984
985  /** Switches on some debugging (only in execCommand() as far as I see at the moment)<br />
986   *
987   * Default: <code>true</code>
988   * @type Boolean
989   */
990  this.debug = true;
991
992  this.URIs =
993  {
994   "blank": _editor_url + "popups/blank.html",
995   "link":  _editor_url + "modules/CreateLink/link.html",
996   "insert_image": _editor_url + "modules/InsertImage/insert_image.html",
997   "insert_table":  _editor_url + "modules/InsertTable/insert_table.html",
998   "select_color": _editor_url + "popups/select_color.html",
999   "about": _editor_url + "popups/about.html",
1000   "help": _editor_url + "popups/editor_help.html"
1001  };
1002
1003
1004   /** The button list conains the definitions of the toolbar button. Normally, there's nothing to change here :)
1005   * <div style="white-space:pre">ADDING CUSTOM BUTTONS: please read below!
1006   * format of the btnList elements is "ID: [ ToolTip, Icon, Enabled in text mode?, ACTION ]"
1007   *    - ID: unique ID for the button.  If the button calls document.execCommand
1008   *        it's wise to give it the same name as the called command.
1009   *    - ACTION: function that gets called when the button is clicked.
1010   *              it has the following prototype:
1011   *                 function(editor, buttonName)
1012   *              - editor is the Xinha object that triggered the call
1013   *              - buttonName is the ID of the clicked button
1014   *              These 2 parameters makes it possible for you to use the same
1015   *              handler for more Xinha objects or for more different buttons.
1016   *    - ToolTip: tooltip, will be translated below
1017   *    - Icon: path to an icon image file for the button
1018   *            OR; you can use an 18x18 block of a larger image by supllying an array
1019   *            that has three elemtents, the first is the larger image, the second is the column
1020   *            the third is the row.  The ros and columns numbering starts at 0 but there is
1021   *            a header row and header column which have numbering to make life easier.
1022   *            See images/buttons_main.gif to see how it's done.
1023   *    - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time.</div>
1024   * @type Object
1025   */
1026  this.btnList =
1027  {
1028    bold: [ "Bold", Xinha._lc({key: 'button_bold', string: ["ed_buttons_main.gif",3,2]}, 'Xinha'), false, function(e) { e.execCommand("bold"); } ],
1029    italic: [ "Italic", Xinha._lc({key: 'button_italic', string: ["ed_buttons_main.gif",2,2]}, 'Xinha'), false, function(e) { e.execCommand("italic"); } ],
1030    underline: [ "Underline", Xinha._lc({key: 'button_underline', string: ["ed_buttons_main.gif",2,0]}, 'Xinha'), false, function(e) { e.execCommand("underline"); } ],
1031    strikethrough: [ "Strikethrough", Xinha._lc({key: 'button_strikethrough', string: ["ed_buttons_main.gif",3,0]}, 'Xinha'), false, function(e) { e.execCommand("strikethrough"); } ],
1032    subscript: [ "Subscript", Xinha._lc({key: 'button_subscript', string: ["ed_buttons_main.gif",3,1]}, 'Xinha'), false, function(e) { e.execCommand("subscript"); } ],
1033    superscript: [ "Superscript", Xinha._lc({key: 'button_superscript', string: ["ed_buttons_main.gif",2,1]}, 'Xinha'), false, function(e) { e.execCommand("superscript"); } ],
1034
1035    justifyleft: [ "Justify Left", ["ed_buttons_main.gif",0,0], false, function(e) { e.execCommand("justifyleft"); } ],
1036    justifycenter: [ "Justify Center", ["ed_buttons_main.gif",1,1], false, function(e){ e.execCommand("justifycenter"); } ],
1037    justifyright: [ "Justify Right", ["ed_buttons_main.gif",1,0], false, function(e) { e.execCommand("justifyright"); } ],
1038    justifyfull: [ "Justify Full", ["ed_buttons_main.gif",0,1], false, function(e) { e.execCommand("justifyfull"); } ],
1039
1040    orderedlist: [ "Ordered List", ["ed_buttons_main.gif",0,3], false, function(e) { e.execCommand("insertorderedlist"); } ],
1041    unorderedlist: [ "Bulleted List", ["ed_buttons_main.gif",1,3], false, function(e) { e.execCommand("insertunorderedlist"); } ],
1042    insertorderedlist: [ "Ordered List", ["ed_buttons_main.gif",0,3], false, function(e) { e.execCommand("insertorderedlist"); } ],
1043    insertunorderedlist: [ "Bulleted List", ["ed_buttons_main.gif",1,3], false, function(e) { e.execCommand("insertunorderedlist"); } ],
1044
1045    outdent: [ "Decrease Indent", ["ed_buttons_main.gif",1,2], false, function(e) { e.execCommand("outdent"); } ],
1046    indent: [ "Increase Indent",["ed_buttons_main.gif",0,2], false, function(e) { e.execCommand("indent"); } ],
1047    forecolor: [ "Font Color", ["ed_buttons_main.gif",3,3], false, function(e) { e.execCommand("forecolor"); } ],
1048    hilitecolor: [ "Background Color", ["ed_buttons_main.gif",2,3], false, function(e) { e.execCommand("hilitecolor"); } ],
1049
1050    undo: [ "Undoes your last action", ["ed_buttons_main.gif",4,2], false, function(e) { e.execCommand("undo"); } ],
1051    redo: [ "Redoes your last action", ["ed_buttons_main.gif",5,2], false, function(e) { e.execCommand("redo"); } ],
1052    cut: [ "Cut selection", ["ed_buttons_main.gif",5,0], false,  function (e, cmd) { e.execCommand(cmd); } ],
1053    copy: [ "Copy selection", ["ed_buttons_main.gif",4,0], false,  function (e, cmd) { e.execCommand(cmd); } ],
1054    paste: [ "Paste from clipboard", ["ed_buttons_main.gif",4,1], false,  function (e, cmd) { e.execCommand(cmd); } ],
1055    selectall: [ "Select all", "ed_selectall.gif", false, function(e) {e.execCommand("selectall");} ],
1056
1057    inserthorizontalrule: [ "Horizontal Rule", ["ed_buttons_main.gif",6,0], false, function(e) { e.execCommand("inserthorizontalrule"); } ],
1058    createlink: [ "Insert Web Link", ["ed_buttons_main.gif",6,1], false, function(e) { e._createLink(); } ],
1059    insertimage: [ "Insert/Modify Image", ["ed_buttons_main.gif",6,3], false, function(e) { e.execCommand("insertimage"); } ],
1060    inserttable: [ "Insert Table", ["ed_buttons_main.gif",6,2], false, function(e) { e.execCommand("inserttable"); } ],
1061
1062    htmlmode: [ "Toggle HTML Source", ["ed_buttons_main.gif",7,0], true, function(e) { e.execCommand("htmlmode"); } ],
1063    toggleborders: [ "Toggle Borders", ["ed_buttons_main.gif",7,2], false, function(e) { e._toggleBorders(); } ],
1064    print: [ "Print document", ["ed_buttons_main.gif",8,1], false, function(e) { if(Xinha.is_gecko) {e._iframe.contentWindow.print(); } else { e.focusEditor(); print(); } } ],
1065    saveas: [ "Save as", "ed_saveas.gif", false, function(e) { e.execCommand("saveas",false,"noname.htm"); } ],
1066    about: [ "About this editor", ["ed_buttons_main.gif",8,2], true, function(e) { e.execCommand("about"); } ],
1067    showhelp: [ "Help using editor", ["ed_buttons_main.gif",9,2], true, function(e) { e.execCommand("showhelp"); } ],
1068
1069    splitblock: [ "Split Block", "ed_splitblock.gif", false, function(e) { e._splitBlock(); } ],
1070    lefttoright: [ "Direction left to right", ["ed_buttons_main.gif",0,4], false, function(e) { e.execCommand("lefttoright"); } ],
1071    righttoleft: [ "Direction right to left", ["ed_buttons_main.gif",1,4], false, function(e) { e.execCommand("righttoleft"); } ],
1072    overwrite: [ "Insert/Overwrite", "ed_overwrite.gif", false, function(e) { e.execCommand("overwrite"); } ],
1073
1074    wordclean: [ "MS Word Cleaner", ["ed_buttons_main.gif",5,3], false, function(e) { e._wordClean(); } ],
1075    clearfonts: [ "Clear Inline Font Specifications", ["ed_buttons_main.gif",5,4], true, function(e) { e._clearFonts(); } ],
1076    removeformat: [ "Remove formatting", ["ed_buttons_main.gif",4,4], false, function(e) { e.execCommand("removeformat"); } ],
1077    killword: [ "Clear MSOffice tags", ["ed_buttons_main.gif",4,3], false, function(e) { e.execCommand("killword"); } ]
1078  };
1079
1080
1081  // initialize tooltips from the I18N module and generate correct image path
1082  for ( var i in this.btnList )
1083  {
1084    var btn = this.btnList[i];
1085    // prevent iterating over wrong type
1086    if ( typeof btn != 'object' )
1087    {
1088      continue;
1089    }
1090    if ( typeof btn[1] != 'string' )
1091    {
1092      btn[1][0] = _editor_url + this.imgURL + btn[1][0];
1093    }
1094    else
1095    {
1096      btn[1] = _editor_url + this.imgURL + btn[1];
1097    }
1098    btn[0] = Xinha._lc(btn[0]); //initialize tooltip
1099  }
1100
1101};
1102/** ADDING CUSTOM BUTTONS
1103*   ---------------------
1104*
1105*
1106* Example on how to add a custom button when you construct the Xinha:
1107*
1108*   var editor = new Xinha("your_text_area_id");
1109*   var cfg = editor.config; // this is the default configuration
1110*   cfg.btnList["my-hilite"] =
1111*       [ function(editor) { editor.surroundHTML('<span style="background:yellow">', '</span>'); }, // action
1112*         "Highlight selection", // tooltip
1113*         "my_hilite.gif", // image
1114*         false // disabled in text mode
1115*       ];
1116*   cfg.toolbar.push(["linebreak", "my-hilite"]); // add the new button to the toolbar
1117*
1118* An alternate (also more convenient and recommended) way to
1119* accomplish this is to use the registerButton function below.
1120*/
1121/** Helper function: register a new button with the configuration.  It can be
1122 * called with all 5 arguments, or with only one (first one).  When called with
1123 * only one argument it must be an object with the following properties: id,
1124 * tooltip, image, textMode, action.<br /> 
1125 *
1126 * Examples:<br />
1127 *<pre>
1128 * config.registerButton("my-hilite", "Hilite text", "my-hilite.gif", false, function(editor) {...});
1129 * config.registerButton({
1130 *      id       : "my-hilite",      // the ID of your button
1131 *      tooltip  : "Hilite text",    // the tooltip
1132 *      image    : "my-hilite.gif",  // image to be displayed in the toolbar
1133 *      textMode : false,            // disabled in text mode
1134 *      action   : function(editor) { // called when the button is clicked
1135 *                   editor.surroundHTML('<span class="hilite">', '</span>');
1136 *                 },
1137 *      context  : "p"               // will be disabled if outside a <p> element
1138 *    });</pre>
1139 */
1140Xinha.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context)
1141{
1142  var the_id;
1143  if ( typeof id == "string" )
1144  {
1145    the_id = id;
1146  }
1147  else if ( typeof id == "object" )
1148  {
1149    the_id = id.id;
1150  }
1151  else
1152  {
1153    alert("ERROR [Xinha.Config::registerButton]:\ninvalid arguments");
1154    return false;
1155  }
1156  // check for existing id
1157//  if(typeof this.customSelects[the_id] != "undefined")
1158//  {
1159    // alert("WARNING [Xinha.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
1160//  }
1161//  if(typeof this.btnList[the_id] != "undefined") {
1162    // alert("WARNING [Xinha.Config::registerDropdown]:\nA button with the same ID already exists.");
1163//  }
1164  switch ( typeof id )
1165  {
1166    case "string":
1167      this.btnList[id] = [ tooltip, image, textMode, action, context ];
1168    break;
1169    case "object":
1170      this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ];
1171    break;
1172  }
1173};
1174
1175Xinha.prototype.registerPanel = function(side, object)
1176{
1177  if ( !side )
1178  {
1179    side = 'right';
1180  }
1181  this.setLoadingMessage('Register ' + side + ' panel ');
1182  var panel = this.addPanel(side);
1183  if ( object )
1184  {
1185    object.drawPanelIn(panel);
1186  }
1187};
1188
1189/** The following helper function registers a dropdown box with the editor
1190 * configuration.  You still have to add it to the toolbar, same as with the
1191 * buttons.  Call it like this:
1192 *
1193 * FIXME: add example
1194 */
1195Xinha.Config.prototype.registerDropdown = function(object)
1196{
1197  // check for existing id
1198//  if ( typeof this.customSelects[object.id] != "undefined" )
1199//  {
1200    // alert("WARNING [Xinha.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
1201//  }
1202//  if ( typeof this.btnList[object.id] != "undefined" )
1203//  {
1204    // alert("WARNING [Xinha.Config::registerDropdown]:\nA button with the same ID already exists.");
1205//  }
1206  this.customSelects[object.id] = object;
1207};
1208
1209/** Call this function to remove some buttons/drop-down boxes from the toolbar.
1210 * Pass as the only parameter a string containing button/drop-down names
1211 * delimited by spaces.  Note that the string should also begin with a space
1212 * and end with a space.  Example:
1213 *
1214 *   config.hideSomeButtons(" fontname fontsize textindicator ");
1215 *
1216 * It's useful because it's easier to remove stuff from the defaul toolbar than
1217 * create a brand new toolbar ;-)
1218 */
1219Xinha.Config.prototype.hideSomeButtons = function(remove)
1220{
1221  var toolbar = this.toolbar;
1222  for ( var i = toolbar.length; --i >= 0; )
1223  {
1224    var line = toolbar[i];
1225    for ( var j = line.length; --j >= 0; )
1226    {
1227      if ( remove.indexOf(" " + line[j] + " ") >= 0 )
1228      {
1229        var len = 1;
1230        if ( /separator|space/.test(line[j + 1]) )
1231        {
1232          len = 2;
1233        }
1234        line.splice(j, len);
1235      }
1236    }
1237  }
1238};
1239
1240/** Helper Function: add buttons/drop-downs boxes with title or separator to the toolbar
1241 * if the buttons/drop-downs boxes doesn't allready exists.
1242 * id: button or selectbox (as array with separator or title)
1243 * where: button or selectbox (as array if the first is not found take the second and so on)
1244 * position:
1245 * -1 = insert button (id) one position before the button (where)
1246 * 0 = replace button (where) by button (id)
1247 * +1 = insert button (id) one position after button (where)
1248 *
1249 * cfg.addToolbarElement(["T[title]", "button_id", "separator"] , ["first_id","second_id"], -1);
1250*/
1251
1252Xinha.Config.prototype.addToolbarElement = function(id, where, position)
1253{
1254  var toolbar = this.toolbar;
1255  var a, i, j, o, sid;
1256  var idIsArray = false;
1257  var whereIsArray = false;
1258  var whereLength = 0;
1259  var whereJ = 0;
1260  var whereI = 0;
1261  var exists = false;
1262  var found = false;
1263  // check if id and where are arrys
1264  if ( ( id && typeof id == "object" ) && ( id.constructor == Array ) )
1265  {
1266    idIsArray = true;
1267  }
1268  if ( ( where && typeof where == "object" ) && ( where.constructor == Array ) )
1269  {
1270    whereIsArray = true;
1271    whereLength = where.length;
1272        }
1273
1274  if ( idIsArray ) //find the button/select box in input array
1275  {
1276    for ( i = 0; i < id.length; ++i )
1277    {
1278      if ( ( id[i] != "separator" ) && ( id[i].indexOf("T[") !== 0) )
1279      {
1280        sid = id[i];
1281      }
1282    }
1283  }
1284  else
1285  {
1286    sid = id;
1287  }
1288 
1289  for ( i = 0; i < toolbar.length; ++i ) {
1290    a = toolbar[i];
1291    for ( j = 0; j < a.length; ++j ) {
1292      // check if button/select box exists
1293      if ( a[j] == sid ) {
1294        return; // cancel to add elements if same button already exists
1295      }
1296    }
1297  }
1298 
1299
1300  for ( i = 0; !found && i < toolbar.length; ++i )
1301  {
1302    a = toolbar[i];
1303    for ( j = 0; !found && j < a.length; ++j )
1304    {
1305      if ( whereIsArray )
1306      {
1307        for ( o = 0; o < whereLength; ++o )
1308        {
1309          if ( a[j] == where[o] )
1310          {
1311            if ( o === 0 )
1312            {
1313              found = true;
1314              j--;
1315              break;
1316            }
1317            else
1318            {
1319              whereI = i;
1320              whereJ = j;
1321              whereLength = o;
1322            }
1323          }
1324        }
1325      }
1326      else
1327      {
1328        // find the position to insert
1329        if ( a[j] == where )
1330        {
1331          found = true;
1332          break;
1333        }
1334      }
1335    }
1336  }
1337
1338  //if check found any other as the first button
1339  if ( !found && whereIsArray )
1340  {
1341    if ( where.length != whereLength )
1342    {
1343      j = whereJ;
1344      a = toolbar[whereI];
1345      found = true;
1346    }
1347  }
1348  if ( found )
1349  {
1350    // replace the found button
1351    if ( position === 0 )
1352    {
1353      if ( idIsArray)
1354      {
1355        a[j] = id[id.length-1];
1356        for ( i = id.length-1; --i >= 0; )
1357        {
1358          a.splice(j, 0, id[i]);
1359        }
1360      }
1361      else
1362      {
1363        a[j] = id;
1364      }
1365    }
1366    else
1367    {
1368      // insert before/after the found button
1369      if ( position < 0 )
1370      {
1371        j = j + position + 1; //correct position before
1372      }
1373      else if ( position > 0 )
1374      {
1375        j = j + position; //correct posion after
1376      }
1377      if ( idIsArray )
1378      {
1379        for ( i = id.length; --i >= 0; )
1380        {
1381          a.splice(j, 0, id[i]);
1382        }
1383      }
1384      else
1385      {
1386        a.splice(j, 0, id);
1387      }
1388    }
1389  }
1390  else
1391  {
1392    // no button found
1393    toolbar[0].splice(0, 0, "separator");
1394    if ( idIsArray)
1395    {
1396      for ( i = id.length; --i >= 0; )
1397      {
1398        toolbar[0].splice(0, 0, id[i]);
1399      }
1400    }
1401    else
1402    {
1403      toolbar[0].splice(0, 0, id);
1404    }
1405  }
1406};
1407/** Alias of Xinha.Config.prototype.hideSomeButtons()
1408* @type Function
1409*/
1410Xinha.Config.prototype.removeToolbarElement = Xinha.Config.prototype.hideSomeButtons;
1411
1412/** Helper function: replace all TEXTAREA-s in the document with Xinha-s.
1413* @param {Xinha.Config} optional config
1414*/
1415Xinha.replaceAll = function(config)
1416{
1417  var tas = document.getElementsByTagName("textarea");
1418  // @todo: weird syntax, doesnt help to read the code, doesnt obfuscate it and doesnt make it quicker, better rewrite this part
1419  for ( var i = tas.length; i > 0; (new Xinha(tas[--i], config)).generate() )
1420  {
1421    // NOP
1422  }
1423};
1424
1425/** Helper function: replaces the TEXTAREA with the given ID with Xinha.
1426* @param {string} id id of the textarea to replace
1427* @param {Xinha.Config} optional config
1428*/
1429Xinha.replace = function(id, config)
1430{
1431  var ta = Xinha.getElementById("textarea", id);
1432  return ta ? (new Xinha(ta, config)).generate() : null;
1433};
1434 
1435/** Creates the toolbar and appends it to the _htmlarea
1436* @private
1437* @returns {DomNode} toolbar
1438*/
1439Xinha.prototype._createToolbar = function ()
1440{
1441  this.setLoadingMessage(Xinha._lc('Create Toolbar'));
1442  var editor = this;    // to access this in nested functions
1443
1444  var toolbar = document.createElement("div");
1445  // ._toolbar is for legacy, ._toolBar is better thanks.
1446  this._toolBar = this._toolbar = toolbar;
1447  toolbar.className = "toolbar";
1448  toolbar.unselectable = "1";
1449  toolbar.align = this.config.toolbarAlign;
1450 
1451  Xinha.freeLater(this, '_toolBar');
1452  Xinha.freeLater(this, '_toolbar');
1453 
1454  var tb_row = null;
1455  var tb_objects = {};
1456  this._toolbarObjects = tb_objects;
1457
1458        this._createToolbar1(editor, toolbar, tb_objects);
1459        this._htmlArea.appendChild(toolbar);     
1460 
1461  return toolbar;
1462};
1463
1464/** FIXME : function never used, can probably be removed from source
1465* @private
1466* @deprecated
1467*/
1468Xinha.prototype._setConfig = function(config)
1469{
1470        this.config = config;
1471};
1472/** FIXME: How can this be used??
1473* @private
1474*/
1475Xinha.prototype._addToolbar = function()
1476{
1477        this._createToolbar1(this, this._toolbar, this._toolbarObjects);
1478};
1479
1480/**
1481 * Create a break element to add in the toolbar
1482 *
1483 * @return {DomNode} HTML element to add
1484 * @private
1485 */
1486Xinha._createToolbarBreakingElement = function()
1487{
1488  var brk = document.createElement('div');
1489  brk.style.height = '1px';
1490  brk.style.width = '1px';
1491  brk.style.lineHeight = '1px';
1492  brk.style.fontSize = '1px';
1493  brk.style.clear = 'both';
1494  return brk;
1495};
1496
1497
1498/** separate from previous createToolBar to allow dynamic change of toolbar
1499 * @private
1500 * @return {DomNode} toolbar
1501 */
1502Xinha.prototype._createToolbar1 = function (editor, toolbar, tb_objects)
1503{
1504  var tb_row;
1505  // This shouldn't be necessary, but IE seems to float outside of the container
1506  // when we float toolbar sections, so we have to clear:both here as well
1507  // as at the end (which we do have to do).
1508  if ( editor.config.flowToolbars )
1509  {
1510    toolbar.appendChild(Xinha._createToolbarBreakingElement());
1511  }
1512
1513  // creates a new line in the toolbar
1514  function newLine()
1515  {
1516    if ( typeof tb_row != 'undefined' && tb_row.childNodes.length === 0)
1517    {
1518      return;
1519    }
1520
1521    var table = document.createElement("table");
1522    table.border = "0px";
1523    table.cellSpacing = "0px";
1524    table.cellPadding = "0px";
1525    if ( editor.config.flowToolbars )
1526    {
1527      if ( Xinha.is_ie )
1528      {
1529        table.style.styleFloat = "left";
1530      }
1531      else
1532      {
1533        table.style.cssFloat = "left";
1534      }
1535    }
1536
1537    toolbar.appendChild(table);
1538    // TBODY is required for IE, otherwise you don't see anything
1539    // in the TABLE.
1540    var tb_body = document.createElement("tbody");
1541    table.appendChild(tb_body);
1542    tb_row = document.createElement("tr");
1543    tb_body.appendChild(tb_row);
1544
1545    table.className = 'toolbarRow'; // meh, kinda.
1546  } // END of function: newLine
1547
1548  // init first line
1549  newLine();
1550
1551  // updates the state of a toolbar element.  This function is member of
1552  // a toolbar element object (unnamed objects created by createButton or
1553  // createSelect functions below).
1554  function setButtonStatus(id, newval)
1555  {
1556    var oldval = this[id];
1557    var el = this.element;
1558    if ( oldval != newval )
1559    {
1560      switch (id)
1561      {
1562        case "enabled":
1563          if ( newval )
1564          {
1565            Xinha._removeClass(el, "buttonDisabled");
1566            el.disabled = false;
1567          }
1568          else
1569          {
1570            Xinha._addClass(el, "buttonDisabled");
1571            el.disabled = true;
1572          }
1573        break;
1574        case "active":
1575          if ( newval )
1576          {
1577            Xinha._addClass(el, "buttonPressed");
1578          }
1579          else
1580          {
1581            Xinha._removeClass(el, "buttonPressed");
1582          }
1583        break;
1584      }
1585      this[id] = newval;
1586    }
1587  } // END of function: setButtonStatus
1588
1589  // this function will handle creation of combo boxes.  Receives as
1590  // parameter the name of a button as defined in the toolBar config.
1591  // This function is called from createButton, above, if the given "txt"
1592  // doesn't match a button.
1593  function createSelect(txt)
1594  {
1595    var options = null;
1596    var el = null;
1597    var cmd = null;
1598    var customSelects = editor.config.customSelects;
1599    var context = null;
1600    var tooltip = "";
1601    switch (txt)
1602    {
1603      case "fontsize":
1604      case "fontname":
1605      case "formatblock":
1606        // the following line retrieves the correct
1607        // configuration option because the variable name
1608        // inside the Config object is named the same as the
1609        // button/select in the toolbar.  For instance, if txt
1610        // == "formatblock" we retrieve config.formatblock (or
1611        // a different way to write it in JS is
1612        // config["formatblock"].
1613        options = editor.config[txt];
1614        cmd = txt;
1615      break;
1616      default:
1617        // try to fetch it from the list of registered selects
1618        cmd = txt;
1619        var dropdown = customSelects[cmd];
1620        if ( typeof dropdown != "undefined" )
1621        {
1622          options = dropdown.options;
1623          context = dropdown.context;
1624          if ( typeof dropdown.tooltip != "undefined" )
1625          {
1626            tooltip = dropdown.tooltip;
1627          }
1628        }
1629        else
1630        {
1631          alert("ERROR [createSelect]:\nCan't find the requested dropdown definition");
1632        }
1633      break;
1634    }
1635    if ( options )
1636    {
1637      el = document.createElement("select");
1638      el.title = tooltip;
1639      var obj =
1640      {
1641        name    : txt, // field name
1642        element : el,   // the UI element (SELECT)
1643        enabled : true, // is it enabled?
1644        text    : false, // enabled in text mode?
1645        cmd     : cmd, // command ID
1646        state   : setButtonStatus, // for changing state
1647        context : context
1648      };
1649     
1650      Xinha.freeLater(obj);
1651     
1652      tb_objects[txt] = obj;
1653     
1654      for ( var i in options )
1655      {
1656        // prevent iterating over wrong type
1657        if ( typeof(options[i]) != 'string' )
1658        {
1659          continue;
1660        }
1661        var op = document.createElement("option");
1662        op.innerHTML = Xinha._lc(i);
1663        op.value = options[i];
1664        el.appendChild(op);
1665      }
1666      Xinha._addEvent(el, "change", function () { editor._comboSelected(el, txt); } );
1667    }
1668    return el;
1669  } // END of function: createSelect
1670
1671  // appends a new button to toolbar
1672  function createButton(txt)
1673  {
1674    // the element that will be created
1675    var el, btn, obj = null;
1676    switch (txt)
1677    {
1678      case "separator":
1679        if ( editor.config.flowToolbars )
1680        {
1681          newLine();
1682        }
1683        el = document.createElement("div");
1684        el.className = "separator";
1685      break;
1686      case "space":
1687        el = document.createElement("div");
1688        el.className = "space";
1689      break;
1690      case "linebreak":
1691        newLine();
1692        return false;
1693      case "textindicator":
1694        el = document.createElement("div");
1695        el.appendChild(document.createTextNode("A"));
1696        el.className = "indicator";
1697        el.title = Xinha._lc("Current style");
1698        obj =
1699        {
1700          name  : txt, // the button name (i.e. 'bold')
1701          element : el, // the UI element (DIV)
1702          enabled : true, // is it enabled?
1703          active        : false, // is it pressed?
1704          text  : false, // enabled in text mode?
1705          cmd   : "textindicator", // the command ID
1706          state : setButtonStatus // for changing state
1707        };
1708     
1709        Xinha.freeLater(obj);
1710     
1711        tb_objects[txt] = obj;
1712      break;
1713      default:
1714        btn = editor.config.btnList[txt];
1715    }
1716    if ( !el && btn )
1717    {
1718      el = document.createElement("a");
1719      el.style.display = 'block';
1720      el.href = 'javascript:void(0)';
1721      el.style.textDecoration = 'none';
1722      el.title = btn[0];
1723      el.className = "button";
1724      el.style.direction = "ltr";
1725      // let's just pretend we have a button object, and
1726      // assign all the needed information to it.
1727      obj =
1728      {
1729        name : txt, // the button name (i.e. 'bold')
1730        element : el, // the UI element (DIV)
1731        enabled : true, // is it enabled?
1732        active : false, // is it pressed?
1733        text : btn[2], // enabled in text mode?
1734        cmd     : btn[3], // the command ID
1735        state   : setButtonStatus, // for changing state
1736        context : btn[4] || null // enabled in a certain context?
1737      };
1738      Xinha.freeLater(el);
1739      Xinha.freeLater(obj);
1740
1741      tb_objects[txt] = obj;
1742
1743      // prevent drag&drop of the icon to content area
1744      el.ondrag = function() { return false; };
1745
1746      // handlers to emulate nice flat toolbar buttons
1747      Xinha._addEvent(
1748        el,
1749        "mouseout",
1750        function(ev)
1751        {
1752          if ( obj.enabled )
1753          {
1754            //Xinha._removeClass(el, "buttonHover");
1755            Xinha._removeClass(el, "buttonActive");
1756            if ( obj.active )
1757            {
1758              Xinha._addClass(el, "buttonPressed");
1759            }
1760          }
1761        }
1762      );
1763
1764      Xinha._addEvent(
1765        el,
1766        "mousedown",
1767        function(ev)
1768        {
1769          if ( obj.enabled )
1770          {
1771            Xinha._addClass(el, "buttonActive");
1772            Xinha._removeClass(el, "buttonPressed");
1773            Xinha._stopEvent(Xinha.is_ie ? window.event : ev);
1774          }
1775        }
1776      );
1777
1778      // when clicked, do the following:
1779      Xinha._addEvent(
1780        el,
1781        "click",
1782        function(ev)
1783        {
1784          ev = Xinha.is_ie ? window.event : ev;
1785          editor.btnClickEvent = ev;
1786          if ( obj.enabled )
1787          {
1788            Xinha._removeClass(el, "buttonActive");
1789            //Xinha._removeClass(el, "buttonHover");
1790            if ( Xinha.is_gecko )
1791            {
1792              editor.activateEditor();
1793            }
1794            obj.cmd(editor, obj.name, obj);
1795            Xinha._stopEvent(ev);
1796          }
1797        }
1798      );
1799
1800      var i_contain = Xinha.makeBtnImg(btn[1]);
1801      var img = i_contain.firstChild;
1802      Xinha.freeLater(i_contain);
1803      Xinha.freeLater(img);
1804     
1805      el.appendChild(i_contain);
1806
1807      obj.imgel = img;     
1808      obj.swapImage = function(newimg)
1809      {
1810        if ( typeof newimg != 'string' )
1811        {
1812          img.src = newimg[0];
1813          img.style.position = 'relative';
1814          img.style.top  = newimg[2] ? ('-' + (18 * (newimg[2] + 1)) + 'px') : '-18px';
1815          img.style.left = newimg[1] ? ('-' + (18 * (newimg[1] + 1)) + 'px') : '-18px';
1816        }
1817        else
1818        {
1819          obj.imgel.src = newimg;
1820          img.style.top = '0px';
1821          img.style.left = '0px';
1822        }
1823      };
1824     
1825    }
1826    else if( !el )
1827    {
1828      el = createSelect(txt);
1829    }
1830
1831    return el;
1832  }
1833
1834  var first = true;
1835  for ( var i = 0; i < this.config.toolbar.length; ++i )
1836  {
1837    if ( !first )
1838    {
1839      // createButton("linebreak");
1840    }
1841    else
1842    {
1843      first = false;
1844    }
1845    if ( this.config.toolbar[i] === null )
1846    {
1847      this.config.toolbar[i] = ['separator'];
1848    }
1849    var group = this.config.toolbar[i];
1850
1851    for ( var j = 0; j < group.length; ++j )
1852    {
1853      var code = group[j];
1854      var tb_cell;
1855      if ( /^([IT])\[(.*?)\]/.test(code) )
1856      {
1857        // special case, create text label
1858        var l7ed = RegExp.$1 == "I"; // localized?
1859        var label = RegExp.$2;
1860        if ( l7ed )
1861        {
1862          label = Xinha._lc(label);
1863        }
1864        tb_cell = document.createElement("td");
1865        tb_row.appendChild(tb_cell);
1866        tb_cell.className = "label";
1867        tb_cell.innerHTML = label;
1868      }
1869      else if ( typeof code != 'function' )
1870      {
1871        var tb_element = createButton(code);
1872        if ( tb_element )
1873        {
1874          tb_cell = document.createElement("td");
1875          tb_cell.className = 'toolbarElement';
1876          tb_row.appendChild(tb_cell);
1877          tb_cell.appendChild(tb_element);
1878        }
1879        else if ( tb_element === null )
1880        {
1881          alert("FIXME: Unknown toolbar item: " + code);
1882        }
1883      }
1884    }
1885  }
1886
1887  if ( editor.config.flowToolbars )
1888  {
1889    toolbar.appendChild(Xinha._createToolbarBreakingElement());
1890  }
1891
1892  return toolbar;
1893};
1894
1895// @todo : is this some kind of test not finished ?
1896//         Why the hell this is not in the config object ?
1897var use_clone_img = false;
1898/** creates a button (i.e. container element + image)
1899 * @private
1900 * @return {DomNode} conteainer element
1901 */
1902Xinha.makeBtnImg = function(imgDef, doc)
1903{
1904  if ( !doc )
1905  {
1906    doc = document;
1907  }
1908
1909  if ( !doc._xinhaImgCache )
1910  {
1911    doc._xinhaImgCache = {};
1912    Xinha.freeLater(doc._xinhaImgCache);
1913  }
1914
1915  var i_contain = null;
1916  if ( Xinha.is_ie && ( ( !doc.compatMode ) || ( doc.compatMode && doc.compatMode == "BackCompat" ) ) )
1917  {
1918    i_contain = doc.createElement('span');
1919  }
1920  else
1921  {
1922    i_contain = doc.createElement('div');
1923    i_contain.style.position = 'relative';
1924  }
1925
1926  i_contain.style.overflow = 'hidden';
1927  i_contain.style.width = "18px";
1928  i_contain.style.height = "18px";
1929  i_contain.className = 'buttonImageContainer';
1930
1931  var img = null;
1932  if ( typeof imgDef == 'string' )
1933  {
1934    if ( doc._xinhaImgCache[imgDef] )
1935    {
1936      img = doc._xinhaImgCache[imgDef].cloneNode();
1937    }
1938    else
1939    {
1940      img = doc.createElement("img");
1941      img.src = imgDef;
1942      img.style.width = "18px";
1943      img.style.height = "18px";
1944      if ( use_clone_img )
1945      {
1946        doc._xinhaImgCache[imgDef] = img.cloneNode();
1947      }
1948    }
1949  }
1950  else
1951  {
1952    if ( doc._xinhaImgCache[imgDef[0]] )
1953    {
1954      img = doc._xinhaImgCache[imgDef[0]].cloneNode();
1955    }
1956    else
1957    {
1958      img = doc.createElement("img");
1959      img.src = imgDef[0];
1960      img.style.position = 'relative';
1961      if ( use_clone_img )
1962      {
1963        doc._xinhaImgCache[imgDef[0]] = img.cloneNode();
1964      }
1965    }
1966    // @todo: Using 18 dont let us use a theme with its own icon toolbar height
1967    //        and width. Probably better to calculate this value 18
1968    //        var sizeIcon = img.width / nb_elements_per_image;
1969    img.style.top  = imgDef[2] ? ('-' + (18 * (imgDef[2] + 1)) + 'px') : '-18px';
1970    img.style.left = imgDef[1] ? ('-' + (18 * (imgDef[1] + 1)) + 'px') : '-18px';
1971  }
1972  i_contain.appendChild(img);
1973  return i_contain;
1974};
1975/** creates the status bar
1976 * @private
1977 * @return {DomNode} status bar
1978 */
1979Xinha.prototype._createStatusBar = function()
1980{
1981  this.setLoadingMessage(Xinha._lc('Create Statusbar'));
1982  var statusbar = document.createElement("div");
1983  statusbar.className = "statusBar";
1984  this._statusBar = statusbar;
1985  Xinha.freeLater(this, '_statusBar');
1986 
1987  // statusbar.appendChild(document.createTextNode(Xinha._lc("Path") + ": "));
1988  // creates a holder for the path view
1989  var div = document.createElement("span");
1990  div.className = "statusBarTree";
1991  div.innerHTML = Xinha._lc("Path") + ": ";
1992
1993  this._statusBarTree = div;
1994  Xinha.freeLater(this, '_statusBarTree');
1995  this._statusBar.appendChild(div);
1996
1997  div = document.createElement("span");
1998  div.innerHTML = Xinha._lc("You are in TEXT MODE.  Use the [<>] button to switch back to WYSIWYG.");
1999  div.style.display = "none";
2000
2001  this._statusBarTextMode = div;
2002  Xinha.freeLater(this, '_statusBarTextMode');
2003  this._statusBar.appendChild(div);
2004
2005  if ( !this.config.statusBar )
2006  {
2007    // disable it...
2008    statusbar.style.display = "none";
2009  }
2010  return statusbar;
2011};
2012
2013/** Creates the Xinha object and replaces the textarea with it. Loads required files.
2014 *  @returns {Boolean}
2015 */
2016Xinha.prototype.generate = function ()
2017{
2018  if ( !Xinha.isSupportedBrowser ) return;
2019 
2020  var i;
2021  var editor = this;  // we'll need "this" in some nested functions
2022  var url;
2023  var found = false;
2024  var links = document.getElementsByTagName("link");
2025
2026  if (!document.getElementById("XinhaCoreDesign"))
2027  {
2028    _editor_css = (typeof _editor_css == "string") ? _editor_css : "Xinha.css";
2029    for(i = 0; i<links.length; i++)
2030    {
2031      if ( ( links[i].rel == "stylesheet" ) && ( links[i].href == _editor_url + _editor_css ) )
2032      {
2033        found = true;
2034      }
2035    }
2036    if ( !found )
2037    {
2038      Xinha.loadStyle(_editor_css,null,"XinhaCoreDesign",true);
2039    }
2040  }
2041 
2042  if ( _editor_skin !== "" && !document.getElementById("XinhaSkin"))
2043  {
2044    found = false;
2045    for(i = 0; i<links.length; i++)
2046    {
2047      if ( ( links[i].rel == "stylesheet" ) && ( links[i].href == _editor_url + 'skins/' + _editor_skin + '/skin.css' ) )
2048      {
2049        found = true;
2050      }
2051    }
2052    if ( !found )
2053    {
2054      Xinha.loadStyle('skins/' + _editor_skin + '/skin.css',null,"XinhaSkin")
2055    }
2056  }
2057 
2058  // Now load a specific browser plugin which will implement the above for us.
2059  if (Xinha.is_ie)
2060  {
2061    url = _editor_url + 'modules/InternetExplorer/InternetExplorer.js';
2062    if ( !Xinha.loadPlugins([{plugin:"InternetExplorer",url:url}], function() { editor.generate(); } ) )
2063    {           
2064      return false;
2065    }
2066    editor._browserSpecificPlugin = editor.registerPlugin('InternetExplorer');
2067  }
2068  else if (Xinha.is_webkit)
2069  {
2070    url = _editor_url + 'modules/WebKit/WebKit.js';
2071    if ( !Xinha.loadPlugins([{plugin:"WebKit",url:url}], function() { editor.generate(); } ) )
2072    {           
2073 
2074      return false;
2075    }
2076    editor._browserSpecificPlugin = editor.registerPlugin('WebKit');
2077  }
2078  else if (Xinha.is_opera)
2079  {
2080    url = _editor_url + 'modules/Opera/Opera.js';
2081    if ( !Xinha.loadPlugins([{plugin:"Opera",url:url}], function() { editor.generate(); } ) )
2082    {           
2083      return false;
2084    }
2085    editor._browserSpecificPlugin = editor.registerPlugin('Opera');
2086  }
2087  else if (Xinha.is_gecko)
2088  {
2089    url = _editor_url + 'modules/Gecko/Gecko.js';
2090    if ( !Xinha.loadPlugins([{plugin:"Gecko",url:url}], function() { editor.generate(); } ) )
2091    {           
2092      return false;
2093    }
2094    editor._browserSpecificPlugin = editor.registerPlugin('Gecko');
2095  }
2096
2097  if ( typeof Dialog == 'undefined' && !Xinha._loadback( _editor_url + 'modules/Dialogs/dialog.js', this.generate, this ) )
2098  {   
2099    return false;
2100  }
2101
2102  if ( typeof Xinha.Dialog == 'undefined' &&  !Xinha._loadback( _editor_url + 'modules/Dialogs/inline-dialog.js' , this.generate, this ) )
2103  {   
2104    return false;
2105  }
2106 
2107  url = _editor_url + 'modules/FullScreen/full-screen.js';
2108  if ( !Xinha.loadPlugins([{plugin:"FullScreen",url:url}], function() { editor.generate(); } ))
2109  {
2110    return false;
2111  }
2112 
2113  url = _editor_url + 'modules/ColorPicker/ColorPicker.js';
2114  if ( !Xinha.loadPlugins([{plugin:"ColorPicker",url:url}], function() { editor.generate(); } ) )
2115  {
2116    return false;
2117  }
2118  else if ( typeof ColorPicker != 'undefined') editor.registerPlugin('ColorPicker');
2119
2120  var toolbar = editor.config.toolbar;
2121  for ( i = toolbar.length; --i >= 0; )
2122  {
2123    for ( var j = toolbar[i].length; --j >= 0; )
2124    {
2125      switch (toolbar[i][j])
2126      {
2127        case "popupeditor":
2128          editor.registerPlugin('FullScreen');
2129        break;
2130        case "insertimage":
2131          url = _editor_url + 'modules/InsertImage/insert_image.js';
2132          if ( typeof Xinha.prototype._insertImage == 'undefined' && !Xinha.loadPlugins([{plugin:"InsertImage",url:url}], function() { editor.generate(); } ) )
2133          {
2134            return false;
2135          }
2136          else if ( typeof InsertImage != 'undefined') editor.registerPlugin('InsertImage');
2137        break;
2138        case "createlink":
2139          url = _editor_url + 'modules/CreateLink/link.js';
2140          if ( typeof Linker == 'undefined' && !Xinha.loadPlugins([{plugin:"CreateLink",url:url}], function() { editor.generate(); } ))
2141          {
2142            return false;
2143          }
2144          else if ( typeof CreateLink != 'undefined') editor.registerPlugin('CreateLink');
2145        break;
2146        case "inserttable":
2147          url = _editor_url + 'modules/InsertTable/insert_table.js';
2148          if ( !Xinha.loadPlugins([{plugin:"InsertTable",url:url}], function() { editor.generate(); } ) )
2149          {
2150            return false;
2151          }
2152          else if ( typeof InsertTable != 'undefined') editor.registerPlugin('InsertTable');
2153        break;
2154      }
2155    }
2156  }
2157
2158  // If this is gecko, set up the paragraph handling now
2159  if ( Xinha.is_gecko &&  editor.config.mozParaHandler != 'built-in' )
2160  {
2161    if (  !Xinha.loadPlugins([{plugin:"EnterParagraphs",url: _editor_url + 'modules/Gecko/paraHandlerBest.js'}], function() { editor.generate(); } ) )
2162    {
2163      return false;
2164    }
2165    editor.registerPlugin('EnterParagraphs');
2166  }
2167  //TEMPORARY FIX FOR IE8 see #1175
2168  if (Xinha.ie_version == 8)
2169  {
2170    this.config.getHtmlMethod = 'TransformInnerHTML';
2171  }
2172
2173  switch (this.config.getHtmlMethod)
2174  {
2175    case 'TransformInnerHTML':
2176      var getHtmlMethodPlugin = _editor_url + 'modules/GetHtml/TransformInnerHTML.js';
2177    break;
2178    default:
2179      var getHtmlMethodPlugin = _editor_url + 'modules/GetHtml/DOMwalk.js';
2180    break;
2181  }
2182 
2183  if ( !Xinha.loadPlugins([{plugin:"GetHtmlImplementation",url:getHtmlMethodPlugin}], function() { editor.generate(); }))
2184  {
2185    return false;       
2186  }
2187  else editor.registerPlugin('GetHtmlImplementation');
2188 
2189  // create the editor framework, yah, table layout I know, but much easier
2190  // to get it working correctly this way, sorry about that, patches welcome.
2191 
2192  this.setLoadingMessage(Xinha._lc('Generate Xinha framework'));
2193 
2194  this._framework =
2195  {
2196    'table':   document.createElement('table'),
2197    'tbody':   document.createElement('tbody'), // IE will not show the table if it doesn't have a tbody!
2198    'tb_row':  document.createElement('tr'),
2199    'tb_cell': document.createElement('td'), // Toolbar
2200
2201    'tp_row':  document.createElement('tr'),
2202    'tp_cell': this._panels.top.container,   // top panel
2203
2204    'ler_row': document.createElement('tr'),
2205    'lp_cell': this._panels.left.container,  // left panel
2206    'ed_cell': document.createElement('td'), // editor
2207    'rp_cell': this._panels.right.container, // right panel
2208
2209    'bp_row':  document.createElement('tr'),
2210    'bp_cell': this._panels.bottom.container,// bottom panel
2211
2212    'sb_row':  document.createElement('tr'),
2213    'sb_cell': document.createElement('td')  // status bar
2214
2215  };
2216  Xinha.freeLater(this._framework);
2217 
2218  var fw = this._framework;
2219  fw.table.border = "0";
2220  fw.table.cellPadding = "0";
2221  fw.table.cellSpacing = "0";
2222
2223  fw.tb_row.style.verticalAlign = 'top';
2224  fw.tp_row.style.verticalAlign = 'top';
2225  fw.ler_row.style.verticalAlign= 'top';
2226  fw.bp_row.style.verticalAlign = 'top';
2227  fw.sb_row.style.verticalAlign = 'top';
2228  fw.ed_cell.style.position     = 'relative';
2229
2230  // Put the cells in the rows        set col & rowspans
2231  // note that I've set all these so that all panels are showing
2232  // but they will be redone in sizeEditor() depending on which
2233  // panels are shown.  It's just here to clarify how the thing
2234  // is put togethor.
2235  fw.tb_row.appendChild(fw.tb_cell);
2236  fw.tb_cell.colSpan = 3;
2237
2238  fw.tp_row.appendChild(fw.tp_cell);
2239  fw.tp_cell.colSpan = 3;
2240
2241  fw.ler_row.appendChild(fw.lp_cell);
2242  fw.ler_row.appendChild(fw.ed_cell);
2243  fw.ler_row.appendChild(fw.rp_cell);
2244
2245  fw.bp_row.appendChild(fw.bp_cell);
2246  fw.bp_cell.colSpan = 3;
2247
2248  fw.sb_row.appendChild(fw.sb_cell);
2249  fw.sb_cell.colSpan = 3;
2250
2251  // Put the rows in the table body
2252  fw.tbody.appendChild(fw.tb_row);  // Toolbar
2253  fw.tbody.appendChild(fw.tp_row); // Left, Top, Right panels
2254  fw.tbody.appendChild(fw.ler_row);  // Editor/Textarea
2255  fw.tbody.appendChild(fw.bp_row);  // Bottom panel
2256  fw.tbody.appendChild(fw.sb_row);  // Statusbar
2257
2258  // and body in the table
2259  fw.table.appendChild(fw.tbody);
2260
2261  var xinha = this._framework.table;
2262  this._htmlArea = xinha;
2263  Xinha.freeLater(this, '_htmlArea');
2264  xinha.className = "htmlarea";
2265
2266    // create the toolbar and put in the area
2267  this._framework.tb_cell.appendChild( this._createToolbar() );
2268
2269    // create the IFRAME & add to container
2270  var iframe = document.createElement("iframe");
2271  iframe.src = this.popupURL(editor.config.URIs.blank);
2272  iframe.id = "XinhaIFrame_" + this._textArea.id;
2273  this._framework.ed_cell.appendChild(iframe);
2274  this._iframe = iframe;
2275  this._iframe.className = 'xinha_iframe';
2276  Xinha.freeLater(this, '_iframe');
2277 
2278    // creates & appends the status bar
2279  var statusbar = this._createStatusBar();
2280  this._framework.sb_cell.appendChild(statusbar);
2281
2282  // insert Xinha before the textarea.
2283  var textarea = this._textArea;
2284  textarea.parentNode.insertBefore(xinha, textarea);
2285  textarea.className = 'xinha_textarea';
2286
2287  // extract the textarea and insert it into the xinha framework
2288  Xinha.removeFromParent(textarea);
2289  this._framework.ed_cell.appendChild(textarea);
2290
2291  // if another editor is activated while this one is in text mode, toolbar is disabled   
2292  Xinha.addDom0Event(
2293  this._textArea,
2294  'click',
2295  function()
2296  {
2297        if ( Xinha._currentlyActiveEditor != this)
2298        {
2299          editor.updateToolbar();
2300        }
2301    return true;
2302  });
2303 
2304  // Set up event listeners for saving the iframe content to the textarea
2305  if ( textarea.form )
2306  {
2307    // onsubmit get the Xinha content and update original textarea.
2308    Xinha.prependDom0Event(
2309      this._textArea.form,
2310      'submit',
2311      function()
2312      {
2313        editor.firePluginEvent('onBeforeSubmit');
2314        editor._textArea.value = editor.outwardHtml(editor.getHTML());
2315        return true;
2316      }
2317    );
2318
2319    var initialTAContent = textarea.value;
2320
2321    // onreset revert the Xinha content to the textarea content
2322    Xinha.prependDom0Event(
2323      this._textArea.form,
2324      'reset',
2325      function()
2326      {
2327        editor.setHTML(editor.inwardHtml(initialTAContent));
2328        editor.updateToolbar();
2329        return true;
2330      }
2331    );
2332
2333    // attach onsubmit handler to form.submit()
2334    // note: catch error in IE if any form element has id="submit"
2335    if ( !textarea.form.xinha_submit )
2336    {
2337      try
2338      {
2339        textarea.form.xinha_submit = textarea.form.submit;
2340        textarea.form.submit = function()
2341        {
2342          this.onsubmit();
2343          this.xinha_submit();
2344        }
2345      } catch(ex) {}
2346    }
2347  }
2348
2349  // add a handler for the "back/forward" case -- on body.unload we save
2350  // the HTML content into the original textarea and restore it in its place.
2351  // apparently this does not work in IE?
2352  Xinha.prependDom0Event(
2353    window,
2354    'unload',
2355    function()
2356    {
2357      editor.firePluginEvent('onBeforeUnload');
2358      textarea.value = editor.outwardHtml(editor.getHTML());
2359      if (!Xinha.is_ie)
2360      {
2361        xinha.parentNode.replaceChild(textarea,xinha);
2362      }
2363      return true;
2364    }
2365  );
2366
2367  // Hide textarea
2368  textarea.style.display = "none";
2369
2370  // Initalize size
2371  editor.initSize();
2372  this.setLoadingMessage(Xinha._lc('Finishing'));
2373  // Add an event to initialize the iframe once loaded.
2374  editor._iframeLoadDone = false;
2375  if (Xinha.is_opera)
2376    {       
2377      editor.initIframe();     
2378    }
2379  else
2380    Xinha._addEvent(
2381      this._iframe,
2382      'load',
2383      function(e)
2384      {
2385        if ( !editor._iframeLoadDone )
2386        {
2387          editor._iframeLoadDone = true;
2388          editor.initIframe();
2389        }
2390        return true;
2391      }
2392    );
2393
2394};
2395
2396/**
2397 * Size the editor according to the INITIAL sizing information.
2398 * config.width
2399 *    The width may be set via three ways
2400 *    auto    = the width is inherited from the original textarea
2401 *    toolbar = the width is set to be the same size as the toolbar
2402 *    <set size> = the width is an explicit size (any CSS measurement, eg 100em should be fine)
2403 *
2404 * config.height
2405 *    auto    = the height is inherited from the original textarea
2406 *    <set size> = an explicit size measurement (again, CSS measurements)
2407 *
2408 * config.sizeIncludesBars
2409 *    true    = the tool & status bars will appear inside the width & height confines
2410 *    false   = the tool & status bars will appear outside the width & height confines
2411 *
2412 * @private
2413 */
2414
2415Xinha.prototype.initSize = function()
2416{
2417  this.setLoadingMessage(Xinha._lc('Init editor size'));
2418  var editor = this;
2419  var width = null;
2420  var height = null;
2421
2422  switch ( this.config.width )
2423  {
2424    case 'auto':
2425      width = this._initial_ta_size.w;
2426    break;
2427
2428    case 'toolbar':
2429      width = this._toolBar.offsetWidth + 'px';
2430    break;
2431
2432    default :
2433      // @todo: check if this is better :
2434      // width = (parseInt(this.config.width, 10) == this.config.width)? this.config.width + 'px' : this.config.width;
2435      width = /[^0-9]/.test(this.config.width) ? this.config.width : this.config.width + 'px';
2436    break;
2437  }
2438
2439  switch ( this.config.height )
2440  {
2441    case 'auto':
2442      height = this._initial_ta_size.h;
2443    break;
2444
2445    default :
2446      // @todo: check if this is better :
2447      // height = (parseInt(this.config.height, 10) == this.config.height)? this.config.height + 'px' : this.config.height;
2448      height = /[^0-9]/.test(this.config.height) ? this.config.height : this.config.height + 'px';
2449    break;
2450  }
2451
2452  this.sizeEditor(width, height, this.config.sizeIncludesBars, this.config.sizeIncludesPanels);
2453
2454  // why can't we use the following line instead ?
2455//  this.notifyOn('panel_change',this.sizeEditor);
2456  this.notifyOn('panel_change',function() { editor.sizeEditor(); });
2457};
2458
2459/**
2460 *  Size the editor to a specific size, or just refresh the size (when window resizes for example)
2461 *  @param {string} width optional width (CSS specification)
2462 *  @param {string} height optional height (CSS specification)
2463 *  @param {Boolean} includingBars optional to indicate if the size should include or exclude tool & status bars
2464 *  @param {Boolean} includingPanels optional to indicate if the size should include or exclude panels
2465 */
2466Xinha.prototype.sizeEditor = function(width, height, includingBars, includingPanels)
2467{
2468  if (this._risizing) return;
2469  this._risizing = true;
2470 
2471  this.notifyOf('before_resize', {width:width, height:height});
2472  this.firePluginEvent('onBeforeResize', width, height);
2473  // We need to set the iframe & textarea to 100% height so that the htmlarea
2474  // isn't "pushed out" when we get it's height, so we can change them later.
2475  this._iframe.style.height   = '100%';
2476  this._textArea.style.height = '100%';
2477  this._iframe.style.width    = '';
2478  this._textArea.style.width  = '';
2479
2480  if ( includingBars !== null )
2481  {
2482    this._htmlArea.sizeIncludesToolbars = includingBars;
2483  }
2484  if ( includingPanels !== null )
2485  {
2486    this._htmlArea.sizeIncludesPanels = includingPanels;
2487  }
2488
2489  if ( width )
2490  {
2491    this._htmlArea.style.width = width;
2492    if ( !this._htmlArea.sizeIncludesPanels )
2493    {
2494      // Need to add some for l & r panels
2495      var rPanel = this._panels.right;
2496      if ( rPanel.on && rPanel.panels.length && Xinha.hasDisplayedChildren(rPanel.div) )
2497      {
2498        this._htmlArea.style.width = (this._htmlArea.offsetWidth + parseInt(this.config.panel_dimensions.right, 10)) + 'px';
2499      }
2500
2501      var lPanel = this._panels.left;
2502      if ( lPanel.on && lPanel.panels.length && Xinha.hasDisplayedChildren(lPanel.div) )
2503      {
2504        this._htmlArea.style.width = (this._htmlArea.offsetWidth + parseInt(this.config.panel_dimensions.left, 10)) + 'px';
2505      }
2506    }
2507  }
2508
2509  if ( height )
2510  {
2511    this._htmlArea.style.height = height;
2512    if ( !this._htmlArea.sizeIncludesToolbars )
2513    {
2514      // Need to add some for toolbars
2515      this._htmlArea.style.height = (this._htmlArea.offsetHeight + this._toolbar.offsetHeight + this._statusBar.offsetHeight) + 'px';
2516    }
2517
2518    if ( !this._htmlArea.sizeIncludesPanels )
2519    {
2520      // Need to add some for t & b panels
2521      var tPanel = this._panels.top;
2522      if ( tPanel.on && tPanel.panels.length && Xinha.hasDisplayedChildren(tPanel.div) )
2523      {
2524        this._htmlArea.style.height = (this._htmlArea.offsetHeight + parseInt(this.config.panel_dimensions.top, 10)) + 'px';
2525      }
2526
2527      var bPanel = this._panels.bottom;
2528      if ( bPanel.on && bPanel.panels.length && Xinha.hasDisplayedChildren(bPanel.div) )
2529      {
2530        this._htmlArea.style.height = (this._htmlArea.offsetHeight + parseInt(this.config.panel_dimensions.bottom, 10)) + 'px';
2531      }
2532    }
2533  }
2534
2535  // At this point we have this._htmlArea.style.width & this._htmlArea.style.height
2536  // which are the size for the OUTER editor area, including toolbars and panels
2537  // now we size the INNER area and position stuff in the right places.
2538  width  = this._htmlArea.offsetWidth;
2539  height = this._htmlArea.offsetHeight;
2540
2541  // Set colspan for toolbar, and statusbar, rowspan for left & right panels, and insert panels to be displayed
2542  // into thier rows
2543  var panels = this._panels;
2544  var editor = this;
2545  var col_span = 1;
2546
2547  function panel_is_alive(pan)
2548  {
2549    if ( panels[pan].on && panels[pan].panels.length && Xinha.hasDisplayedChildren(panels[pan].container) )
2550    {
2551      panels[pan].container.style.display = '';
2552      return true;
2553    }
2554    // Otherwise make sure it's been removed from the framework
2555    else
2556    {
2557      panels[pan].container.style.display='none';
2558      return false;
2559    }
2560  }
2561
2562  if ( panel_is_alive('left') )
2563  {
2564    col_span += 1;     
2565  }
2566
2567//  if ( panel_is_alive('top') )
2568//  {
2569    // NOP
2570//  }
2571
2572  if ( panel_is_alive('right') )
2573  {
2574    col_span += 1;
2575  }
2576
2577//  if ( panel_is_alive('bottom') )
2578//  {
2579    // NOP
2580//  }
2581
2582  this._framework.tb_cell.colSpan = col_span;
2583  this._framework.tp_cell.colSpan = col_span;
2584  this._framework.bp_cell.colSpan = col_span;
2585  this._framework.sb_cell.colSpan = col_span;
2586
2587  // Put in the panel rows, top panel goes above editor row
2588  if ( !this._framework.tp_row.childNodes.length )
2589  {
2590    Xinha.removeFromParent(this._framework.tp_row);
2591  }
2592  else
2593  {
2594    if ( !Xinha.hasParentNode(this._framework.tp_row) )
2595    {
2596      this._framework.tbody.insertBefore(this._framework.tp_row, this._framework.ler_row);
2597    }
2598  }
2599
2600  // bp goes after the editor
2601  if ( !this._framework.bp_row.childNodes.length )
2602  {
2603    Xinha.removeFromParent(this._framework.bp_row);
2604  }
2605  else
2606  {
2607    if ( !Xinha.hasParentNode(this._framework.bp_row) )
2608    {
2609      this._framework.tbody.insertBefore(this._framework.bp_row, this._framework.ler_row.nextSibling);
2610    }
2611  }
2612
2613  // finally if the statusbar is on, insert it
2614  if ( !this.config.statusBar )
2615  {
2616    Xinha.removeFromParent(this._framework.sb_row);
2617  }
2618  else
2619  {
2620    if ( !Xinha.hasParentNode(this._framework.sb_row) )
2621    {
2622      this._framework.table.appendChild(this._framework.sb_row);
2623    }
2624  }
2625
2626  // Size and set colspans, link up the framework
2627  this._framework.lp_cell.style.width  = this.config.panel_dimensions.left;
2628  this._framework.rp_cell.style.width  = this.config.panel_dimensions.right;
2629  this._framework.tp_cell.style.height = this.config.panel_dimensions.top;
2630  this._framework.bp_cell.style.height = this.config.panel_dimensions.bottom;
2631  this._framework.tb_cell.style.height = this._toolBar.offsetHeight + 'px';
2632  this._framework.sb_cell.style.height = this._statusBar.offsetHeight + 'px';
2633
2634  var edcellheight = height - this._toolBar.offsetHeight - this._statusBar.offsetHeight;
2635  if ( panel_is_alive('top') )
2636  {
2637    edcellheight -= parseInt(this.config.panel_dimensions.top, 10);
2638  }
2639  if ( panel_is_alive('bottom') )
2640  {
2641    edcellheight -= parseInt(this.config.panel_dimensions.bottom, 10);
2642  }
2643  this._iframe.style.height = edcellheight + 'px'; 
2644 
2645  var edcellwidth = width;
2646  if ( panel_is_alive('left') )
2647  {
2648    edcellwidth -= parseInt(this.config.panel_dimensions.left, 10);
2649  }
2650  if ( panel_is_alive('right') )
2651  {
2652    edcellwidth -= parseInt(this.config.panel_dimensions.right, 10);   
2653  }
2654  var iframeWidth = (this.config.iframeWidth)? parseInt(this.config.iframeWidth,10): null;
2655  this._iframe.style.width = (iframeWidth && iframeWidth < edcellwidth)? iframeWidth + "px": edcellwidth + "px";
2656
2657  this._textArea.style.height = this._iframe.style.height;
2658  this._textArea.style.width  = this._iframe.style.width;
2659     
2660  this.notifyOf('resize', {width:this._htmlArea.offsetWidth, height:this._htmlArea.offsetHeight});
2661  this.firePluginEvent('onResize',this._htmlArea.offsetWidth, this._htmlArea.offsetWidth);
2662  this._risizing = false;
2663};
2664/** FIXME: Never used, what is this for?
2665* @param {string} side
2666* @param {Object}
2667*/
2668Xinha.prototype.registerPanel = function(side, object)
2669{
2670  if ( !side )
2671  {
2672    side = 'right';
2673  }
2674  this.setLoadingMessage('Register ' + side + ' panel ');
2675  var panel = this.addPanel(side);
2676  if ( object )
2677  {
2678    object.drawPanelIn(panel);
2679  }
2680};
2681/** Creates a panel in the panel container on the specified side
2682* @param {String} side the panel container to which the new panel will be added<br />
2683*                                                                       Possible values are: "right","left","top","bottom"
2684* @returns {DomNode} Panel div
2685*/
2686Xinha.prototype.addPanel = function(side)
2687{
2688  var div = document.createElement('div');
2689  div.side = side;
2690  if ( side == 'left' || side == 'right' )
2691  {
2692    div.style.width  = this.config.panel_dimensions[side];
2693    if(this._iframe) div.style.height = this._iframe.style.height;     
2694  }
2695  Xinha.addClasses(div, 'panel');
2696  this._panels[side].panels.push(div);
2697  this._panels[side].div.appendChild(div);
2698
2699  this.notifyOf('panel_change', {'action':'add','panel':div});
2700  this.firePluginEvent('onPanelChange','add',div);
2701  return div;
2702};
2703/** Removes a panel
2704* @param {DomNode} panel object as returned by Xinha.prototype.addPanel()
2705*/
2706Xinha.prototype.removePanel = function(panel)
2707{
2708  this._panels[panel.side].div.removeChild(panel);
2709  var clean = [];
2710  for ( var i = 0; i < this._panels[panel.side].panels.length; i++ )
2711  {
2712    if ( this._panels[panel.side].panels[i] != panel )
2713    {
2714      clean.push(this._panels[panel.side].panels[i]);
2715    }
2716  }
2717  this._panels[panel.side].panels = clean;
2718  this.notifyOf('panel_change', {'action':'remove','panel':panel});
2719  this.firePluginEvent('onPanelChange','remove',panel);
2720};
2721/** Hides a panel
2722* @param {DomNode} panel object as returned by Xinha.prototype.addPanel()
2723*/
2724Xinha.prototype.hidePanel = function(panel)
2725{
2726  if ( panel && panel.style.display != 'none' )
2727  {
2728    try { var pos = this.scrollPos(this._iframe.contentWindow); } catch(e) { }
2729    panel.style.display = 'none';
2730    this.notifyOf('panel_change', {'action':'hide','panel':panel});
2731    this.firePluginEvent('onPanelChange','hide',panel);
2732    try { this._iframe.contentWindow.scrollTo(pos.x,pos.y)} catch(e) { }
2733  }
2734};
2735/** Shows a panel
2736* @param {DomNode} panel object as returned by Xinha.prototype.addPanel()
2737*/
2738Xinha.prototype.showPanel = function(panel)
2739{
2740  if ( panel && panel.style.display == 'none' )
2741  {
2742    try { var pos = this.scrollPos(this._iframe.contentWindow); } catch(e) {}
2743    panel.style.display = '';
2744    this.notifyOf('panel_change', {'action':'show','panel':panel});
2745    this.firePluginEvent('onPanelChange','show',panel);
2746    try { this._iframe.contentWindow.scrollTo(pos.x,pos.y)} catch(e) { }
2747  }
2748};
2749/** Hides the panel(s) on one or more sides
2750* @param {Array} sides the sides on which the panels shall be hidden
2751*/
2752Xinha.prototype.hidePanels = function(sides)
2753{
2754  if ( typeof sides == 'undefined' )
2755  {
2756    sides = ['left','right','top','bottom'];
2757  }
2758
2759  var reShow = [];
2760  for ( var i = 0; i < sides.length;i++ )
2761  {
2762    if ( this._panels[sides[i]].on )
2763    {
2764      reShow.push(sides[i]);
2765      this._panels[sides[i]].on = false;
2766    }
2767  }
2768  this.notifyOf('panel_change', {'action':'multi_hide','sides':sides});
2769  this.firePluginEvent('onPanelChange','multi_hide',sides);
2770};
2771/** Shows the panel(s) on one or more sides
2772* @param {Array} sides the sides on which the panels shall be hidden
2773*/
2774Xinha.prototype.showPanels = function(sides)
2775{
2776  if ( typeof sides == 'undefined' )
2777  {
2778    sides = ['left','right','top','bottom'];
2779  }
2780
2781  var reHide = [];
2782  for ( var i = 0; i < sides.length; i++ )
2783  {
2784    if ( !this._panels[sides[i]].on )
2785    {
2786      reHide.push(sides[i]);
2787      this._panels[sides[i]].on = true;
2788    }
2789  }
2790  this.notifyOf('panel_change', {'action':'multi_show','sides':sides});
2791  this.firePluginEvent('onPanelChange','multi_show',sides);
2792};
2793/** Returns an array containig all properties that are set in an object
2794* @param {Object} obj
2795* @returns {Array}
2796*/
2797Xinha.objectProperties = function(obj)
2798{
2799  var props = [];
2800  for ( var x in obj )
2801  {
2802    props[props.length] = x;
2803  }
2804  return props;
2805};
2806
2807/** Checks if editor is active
2808 *<br />
2809 * EDITOR ACTIVATION NOTES:<br />
2810 *  when a page has multiple Xinha editors, ONLY ONE should be activated at any time (this is mostly to
2811 *  work around a bug in Mozilla, but also makes some sense).  No editor should be activated or focused
2812 *  automatically until at least one editor has been activated through user action (by mouse-clicking in
2813 *  the editor).
2814 * @private
2815 * @returns {Boolean}
2816 */
2817Xinha.prototype.editorIsActivated = function()
2818{
2819  try
2820  {
2821    return Xinha.is_designMode ? this._doc.designMode == 'on' : this._doc.body.contentEditable;
2822  }
2823  catch (ex)
2824  {
2825    return false;
2826  }
2827};
2828/**  We need to know that at least one editor on the page has been activated
2829*    this is because we will not focus any editor until an editor has been activated
2830* @private
2831* @type {Boolean}
2832*/
2833Xinha._someEditorHasBeenActivated = false;
2834/**  Stores a reference to the currently active editor
2835* @private
2836* @type {Xinha}
2837*/
2838Xinha._currentlyActiveEditor      = null;
2839/** Enables one editor for editing, e.g. by a click in the editing area or after it has been
2840 *  deactivated programmatically before
2841 * @private
2842 * @returns {Boolean}
2843 */
2844Xinha.prototype.activateEditor = function()
2845{
2846  // We only want ONE editor at a time to be active
2847  if ( Xinha._currentlyActiveEditor )
2848  {
2849    if ( Xinha._currentlyActiveEditor == this )
2850    {
2851      return true;
2852    }
2853    Xinha._currentlyActiveEditor.deactivateEditor();
2854  }
2855
2856  if ( Xinha.is_designMode && this._doc.designMode != 'on' )
2857  {
2858    try
2859    {
2860      // cannot set design mode if no display
2861      if ( this._iframe.style.display == 'none' )
2862      {
2863        this._iframe.style.display = '';
2864        this._doc.designMode = 'on';
2865        this._iframe.style.display = 'none';
2866      }
2867      else
2868      {
2869        this._doc.designMode = 'on';
2870      }
2871    } catch (ex) {}
2872  }
2873  else if ( Xinha.is_ie&& this._doc.body.contentEditable !== true )
2874  {
2875    this._doc.body.contentEditable = true;
2876  }
2877
2878  Xinha._someEditorHasBeenActivated = true;
2879  Xinha._currentlyActiveEditor      = this;
2880
2881  var editor = this;
2882  this.enableToolbar();
2883};
2884/** Disables the editor
2885 * @private
2886 */
2887Xinha.prototype.deactivateEditor = function()
2888{
2889  // If the editor isn't active then the user shouldn't use the toolbar
2890  this.disableToolbar();
2891
2892  if ( Xinha.is_designMode && this._doc.designMode != 'off' )
2893  {
2894    try
2895    {
2896      this._doc.designMode = 'off';
2897    } catch (ex) {}
2898  }
2899  else if ( !Xinha.is_designMode && this._doc.body.contentEditable !== false )
2900  {
2901    this._doc.body.contentEditable = false;
2902  }
2903
2904  if ( Xinha._currentlyActiveEditor != this )
2905  {
2906    // We just deactivated an editor that wasn't marked as the currentlyActiveEditor
2907
2908    return; // I think this should really be an error, there shouldn't be a situation where
2909            // an editor is deactivated without first being activated.  but it probably won't
2910            // hurt anything.
2911  }
2912
2913  Xinha._currentlyActiveEditor = false;
2914};
2915/** Creates the iframe (editable area)
2916 * @private
2917 */
2918Xinha.prototype.initIframe = function()
2919{
2920  this.disableToolbar();
2921  var doc = null;
2922  var editor = this;
2923  try
2924  {
2925    if ( editor._iframe.contentDocument )
2926    {
2927      this._doc = editor._iframe.contentDocument;       
2928    }
2929    else
2930    {
2931      this._doc = editor._iframe.contentWindow.document;
2932    }
2933    doc = this._doc;
2934    // try later
2935    if ( !doc )
2936    {
2937      if ( Xinha.is_gecko )
2938      {
2939        setTimeout(function() { editor.initIframe(); }, 50);
2940        return false;
2941      }
2942      else
2943      {
2944        alert("ERROR: IFRAME can't be initialized.");
2945      }
2946    }
2947  }
2948  catch(ex)
2949  { // try later
2950    setTimeout(function() { editor.initIframe(); }, 50);
2951  }
2952 
2953  Xinha.freeLater(this, '_doc');
2954
2955  doc.open("text/html","replace");
2956  var html = '';
2957  if ( editor.config.browserQuirksMode === false )
2958  {
2959    var doctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
2960  }
2961  else if ( editor.config.browserQuirksMode === true )
2962  {
2963     var doctype = '';
2964  }
2965  else
2966  {
2967     var doctype = Xinha.getDoctype(document);
2968  }
2969  if ( !editor.config.fullPage )
2970  {
2971    html += doctype + "\n";
2972    html += "<html>\n";
2973    html += "<head>\n";
2974    html += "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + editor.config.charSet + "\">\n";
2975    if ( typeof editor.config.baseHref != 'undefined' && editor.config.baseHref !== null )
2976    {
2977      html += "<base href=\"" + editor.config.baseHref + "\"/>\n";
2978    }
2979   
2980    html += Xinha.addCoreCSS();
2981   
2982    if ( typeof editor.config.pageStyleSheets !== 'undefined' )
2983    {
2984      for ( var i = 0; i < editor.config.pageStyleSheets.length; i++ )
2985      {
2986        if ( editor.config.pageStyleSheets[i].length > 0 )
2987        {
2988          html += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + editor.config.pageStyleSheets[i] + "\">";
2989          //html += "<style> @import url('" + editor.config.pageStyleSheets[i] + "'); </style>\n";
2990        }
2991      }
2992    }
2993   
2994    if ( editor.config.pageStyle )
2995    {
2996      html += "<style type=\"text/css\">\n" + editor.config.pageStyle + "\n</style>";
2997    }
2998   
2999    html += "</head>\n";
3000    html += "<body" + (editor.config.bodyID ? (" id=\"" + editor.config.bodyID + "\"") : '') + ">\n";
3001    html +=   editor.inwardHtml(editor._textArea.value);
3002    html += "</body>\n";
3003    html += "</html>";
3004  }
3005  else
3006  {
3007    html = editor.inwardHtml(editor._textArea.value);
3008    if ( html.match(Xinha.RE_doctype) )
3009    {
3010      editor.setDoctype(RegExp.$1);
3011      //html = html.replace(Xinha.RE_doctype, "");
3012    }
3013   
3014    //Fix Firefox problem with link elements not in right place (just before head)
3015    var match = html.match(/<link\s+[\s\S]*?["']\s*\/?>/gi);
3016    html = html.replace(/<link\s+[\s\S]*?["']\s*\/?>\s*/gi, '');
3017    match ? html = html.replace(/<\/head>/i, match.join('\n') + "\n</head>") : null;   
3018  }
3019  doc.write(html);
3020  doc.close();
3021  if ( this.config.fullScreen )
3022  {
3023    this._fullScreen();
3024  }
3025  this.setEditorEvents();
3026};
3027 
3028/**
3029 * Delay a function until the document is ready for operations.
3030 * See ticket:547
3031 * @public
3032 * @param {Function} f  The function to call once the document is ready
3033 */
3034Xinha.prototype.whenDocReady = function(f)
3035{
3036  var e = this;
3037  if ( this._doc && this._doc.body )
3038  {
3039    f();
3040  }
3041  else
3042  {
3043    setTimeout(function() { e.whenDocReady(f); }, 50);
3044  }
3045};
3046
3047
3048/** Switches editor mode between wysiwyg and text (HTML)
3049 * @param {String} mode optional "textmode" or "wysiwyg", if omitted, toggles between modes.
3050 */
3051Xinha.prototype.setMode = function(mode)
3052{
3053  var html;
3054  if ( typeof mode == "undefined" )
3055  {
3056    mode = this._editMode == "textmode" ? "wysiwyg" : "textmode";
3057  }
3058  switch ( mode )
3059  {
3060    case "textmode":
3061      this.firePluginEvent('onBeforeMode', 'textmode');
3062      this.setCC("iframe");
3063      html = this.outwardHtml(this.getHTML());
3064      this.setHTML(html);
3065
3066      // Hide the iframe
3067      this.deactivateEditor();
3068      this._iframe.style.display   = 'none';
3069      this._textArea.style.display = '';
3070
3071      if ( this.config.statusBar )
3072      {
3073        this._statusBarTree.style.display = "none";
3074        this._statusBarTextMode.style.display = "";
3075      }
3076      this.findCC("textarea");
3077      this.notifyOf('modechange', {'mode':'text'});
3078      this.firePluginEvent('onMode', 'textmode');
3079    break;
3080
3081    case "wysiwyg":
3082      this.firePluginEvent('onBeforeMode', 'wysiwyg');
3083      this.setCC("textarea");
3084      html = this.inwardHtml(this.getHTML());
3085      this.deactivateEditor();
3086      this.setHTML(html);
3087      this._iframe.style.display   = '';
3088      this._textArea.style.display = "none";
3089      this.activateEditor();
3090      if ( this.config.statusBar )
3091      {
3092        this._statusBarTree.style.display = "";
3093        this._statusBarTextMode.style.display = "none";
3094      }
3095      this.findCC("iframe");
3096      this.notifyOf('modechange', {'mode':'wysiwyg'});
3097      this.firePluginEvent('onMode', 'wysiwyg');
3098
3099    break;
3100
3101    default:
3102      alert("Mode <" + mode + "> not defined!");
3103      return false;
3104  }
3105  this._editMode = mode;
3106};
3107/** Sets the HTML in fullpage mode. Actually the whole iframe document is rewritten.
3108 * @private
3109 * @param {String} html
3110 */
3111Xinha.prototype.setFullHTML = function(html)
3112{
3113  var save_multiline = RegExp.multiline;
3114  RegExp.multiline = true;
3115  if ( html.match(Xinha.RE_doctype) )
3116  {
3117    this.setDoctype(RegExp.$1);
3118   // html = html.replace(Xinha.RE_doctype, "");
3119  }
3120  RegExp.multiline = save_multiline;
3121  // disabled to save body attributes see #459
3122  if ( 0 )
3123  {
3124    if ( html.match(Xinha.RE_head) )
3125    {
3126      this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1;
3127    }
3128    if ( html.match(Xinha.RE_body) )
3129    {
3130      this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1;
3131    }
3132  }
3133  else
3134  {
3135    // FIXME - can we do this without rewriting the entire document
3136    //  does the above not work for IE?
3137    var reac = this.editorIsActivated();
3138    if ( reac )
3139    {
3140      this.deactivateEditor();
3141    }
3142    var html_re = /<html>((.|\n)*?)<\/html>/i;
3143    html = html.replace(html_re, "$1");
3144    this._doc.open("text/html","replace");
3145    this._doc.write(html);
3146    this._doc.close();
3147    if ( reac )
3148    {
3149      this.activateEditor();
3150    }       
3151    this.setEditorEvents();
3152    return true;
3153  }
3154};
3155/** Initialize some event handlers
3156 * @private
3157 */
3158Xinha.prototype.setEditorEvents = function()
3159{
3160  var editor=this;
3161  var doc = this._doc;
3162
3163  editor.whenDocReady(
3164    function()
3165    {
3166      // if we have multiple editors some bug in Mozilla makes some lose editing ability
3167      Xinha._addEvents(
3168        doc,
3169        ["mousedown"],
3170        function()
3171        {
3172          editor.activateEditor();
3173          return true;
3174        }
3175      );
3176      if (Xinha.is_ie)
3177      { // #1019 Cusor not jumping to editable part of window when clicked in IE, see also #1039
3178        Xinha._addEvent(
3179        editor._doc.getElementsByTagName("html")[0],
3180        "click",
3181          function()
3182          {
3183            if (editor._iframe.contentWindow.event.srcElement.tagName.toLowerCase() == 'html') // if  clicked below the text (=body), the text cursor does not appear, see #1019
3184            {
3185               var r = editor._doc.body.createTextRange();
3186               r.collapse(); 
3187               r.select()
3188               //setTimeout (function () { r.collapse();  r.select();},100); // won't do without timeout, dunno why
3189             }
3190             return true;
3191          }
3192        );
3193      }
3194
3195      // intercept some events; for updating the toolbar & keyboard handlers
3196      Xinha._addEvents(
3197        doc,
3198        ["keydown", "keypress", "mousedown", "mouseup", "drag"],
3199        function (event)
3200        {
3201          return editor._editorEvent(Xinha.is_ie ? editor._iframe.contentWindow.event : event);
3202        }
3203      );
3204
3205      // FIXME - this needs to be cleaned up and use editor.firePluginEvent
3206      //  I don't like both onGenerate and onGenerateOnce, we should only
3207      //  have onGenerate and it should only be called when the editor is
3208      //  generated (once and only once)
3209      // check if any plugins have registered refresh handlers
3210      for ( var i in editor.plugins )
3211      {
3212        var plugin = editor.plugins[i].instance;
3213        Xinha.refreshPlugin(plugin);
3214      }
3215
3216      // specific editor initialization
3217      if ( typeof editor._onGenerate == "function" )
3218      {
3219        editor._onGenerate();
3220      }
3221
3222      Xinha.addDom0Event(window, 'resize', function(e) { editor.sizeEditor(); });
3223      editor.removeLoadingMessage();
3224    }
3225  );
3226};
3227 
3228/***************************************************
3229 *  Category: PLUGINS
3230 ***************************************************/
3231
3232
3233/** Create the specified plugin and register it with this Xinha
3234 *  return the plugin created to allow refresh when necessary.<br />
3235 *  <strong>This is only useful if Xinha is generated without using Xinha.makeEditors()</strong>
3236 */
3237Xinha.prototype.registerPlugin = function()
3238{
3239  if ( !Xinha.isSupportedBrowser ) return;
3240 
3241  var plugin = arguments[0];
3242
3243  // We can only register plugins that have been succesfully loaded
3244  if ( plugin === null || typeof plugin == 'undefined' || (typeof plugin == 'string' && typeof window[plugin] == 'undefined') )
3245  {
3246    return false;
3247  }
3248
3249  var args = [];
3250  for ( var i = 1; i < arguments.length; ++i )
3251  {
3252    args.push(arguments[i]);
3253  }
3254  return this.registerPlugin2(plugin, args);
3255};
3256/** This is the variant of the function above where the plugin arguments are
3257 * already packed in an array.  Externally, it should be only used in the
3258 * full-screen editor code, in order to initialize plugins with the same
3259 * parameters as in the opener window.
3260 * @private
3261 */
3262Xinha.prototype.registerPlugin2 = function(plugin, args)
3263{
3264  if ( typeof plugin == "string" && typeof window[plugin] == 'function' )
3265  {
3266    plugin = window[plugin];
3267  }
3268  if ( typeof plugin == "undefined" )
3269  {
3270    /* FIXME: This should never happen. But why does it do? */
3271    return false;
3272  }
3273  var obj = new plugin(this, args);
3274  if ( obj )
3275  {
3276    var clone = {};
3277    var info = plugin._pluginInfo;
3278    for ( var i in info )
3279    {
3280      clone[i] = info[i];
3281    }
3282    clone.instance = obj;
3283    clone.args = args;
3284    this.plugins[plugin._pluginInfo.name] = clone;
3285    return obj;
3286  }
3287  else
3288  {
3289    alert("Can't register plugin " + plugin.toString() + ".");
3290  }
3291};
3292
3293
3294/** Dynamically returns the directory from which the plugins are loaded<br />
3295 *  This could be overridden to change the dir<br />
3296 *  @TODO: Wouldn't this be better as a config option?
3297 * @private
3298 * @param {String} pluginName
3299 * @returns {String} path to plugin
3300 */
3301Xinha.getPluginDir = function(plugin)
3302{
3303  if (Xinha.externalPlugins[plugin])
3304  {
3305    return Xinha.externalPlugins[plugin][0];
3306  }
3307  else
3308  {
3309    return _editor_url + "plugins/" + plugin ;
3310  }
3311}
3312/** Static function that loads the given plugin
3313 * @param {String} pluginName
3314 * @param {Function} callback function to be called when file is loaded
3315 * @param {String} plugin_file URL of the file to load
3316 * @returns {Boolean} true if plugin loaded, false otherwise
3317 */
3318Xinha.loadPlugin = function(pluginName, callback, plugin_file)
3319{
3320  if ( !Xinha.isSupportedBrowser ) return;
3321 
3322  Xinha.setLoadingMessage (Xinha._lc("Loading plugin $plugin="+pluginName+"$"));
3323
3324  // Might already be loaded
3325  if ( typeof window['pluginName'] != 'undefined' )
3326  {
3327    if ( callback )
3328    {
3329      callback(pluginName);
3330    }
3331    return true;
3332  }
3333
3334  if(!plugin_file)
3335  {
3336    var dir = this.getPluginDir(pluginName);
3337    var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g, function (str, l1, l2, l3) { return l1 + "-" + l2.toLowerCase() + l3; }).toLowerCase() + ".js";
3338    plugin_file = dir + "/" + plugin;
3339  }
3340 
3341  Xinha._loadback(plugin_file, callback ? function() { callback(pluginName); } : null);
3342  return false;
3343};
3344/** Stores a status for each loading plugin that may be one of "loading","ready", or "failed"
3345 * @private
3346 * @type {Object}
3347 */
3348Xinha._pluginLoadStatus = {};
3349
3350Xinha.externalPlugins = {};
3351
3352/** Static function that loads the plugins (see xinha_plugins in NewbieGuide)
3353 * @param {Array} plugins
3354 * @param {Function} callbackIfNotReady function that is called repeatedly until all files are
3355 * @param {String} optional url URL of the plugin file; obviously plugins should contain only one item if url is given
3356 * @returns {Boolean} true if all plugins are loaded, false otherwise
3357 */
3358Xinha.loadPlugins = function(plugins, callbackIfNotReady,url)
3359{
3360  if ( !Xinha.isSupportedBrowser ) return;
3361  Xinha.setLoadingMessage (Xinha._lc("Loading plugins"));
3362  var m;
3363  for (var i=0;i<plugins.length;i++)
3364  {
3365    if (typeof plugins[i] == 'object')
3366    {
3367      m = plugins[i].url.match(/(.*)(\/[^\/]*)$/);
3368      Xinha.externalPlugins[plugins[i].plugin] = [m[1],m[2]];
3369      plugins[i] = plugins[i].plugin;
3370    }
3371  }
3372 
3373  // Rip the ones that are loaded and look for ones that have failed
3374  var retVal = true;
3375  var nuPlugins = Xinha.cloneObject(plugins);
3376
3377  while ( nuPlugins.length )
3378  {
3379    var p = nuPlugins.pop();
3380    if (p == 'FullScreen' && !Xinha.externalPlugins['FullScreen'] ) continue; //prevent trying to load FullScreen plugin from the plugins folder
3381   
3382    if ( typeof Xinha._pluginLoadStatus[p] == 'undefined' )
3383    {
3384      // Load it
3385      Xinha._pluginLoadStatus[p] = 'loading';
3386      Xinha.loadPlugin(p,
3387        function(plugin)
3388        {
3389          if ( typeof window[plugin] != 'undefined' )
3390          {
3391            Xinha._pluginLoadStatus[plugin] = 'ready';
3392          }
3393          else
3394          {
3395            // Actually, this won't happen, because if the script fails
3396            // it will throw an exception preventing the callback from
3397            // running.  This will leave it always in the "loading" state
3398            // unfortunatly that means we can't fail plugins gracefully
3399            // by just skipping them.
3400            Xinha._pluginLoadStatus[plugin] = 'failed';
3401          }
3402        },(Xinha.externalPlugins[p] ? Xinha.externalPlugins[p][0]+Xinha.externalPlugins[p][1] : url)
3403      );
3404      retVal = false;
3405    }
3406    else
3407    {
3408      // @todo: a simple (if) would not be better than this tortuous (switch) structure ?
3409      // if ( Xinha._pluginLoadStatus[p] !== 'failed' && Xinha._pluginLoadStatus[p] !== 'ready' )
3410      // {
3411      //   retVal = false;
3412      // }
3413      switch ( Xinha._pluginLoadStatus[p] )
3414      {
3415        case 'failed':
3416        case 'ready' :
3417        break;
3418
3419        //case 'loading':
3420        default       :
3421         retVal = false;
3422       break;
3423      }
3424    }
3425  }
3426
3427  // All done, just return
3428  if ( retVal )
3429  {
3430    return true;
3431  }
3432
3433  // Waiting on plugins to load, return false now and come back a bit later
3434  // if we have to callback
3435  if ( callbackIfNotReady )
3436  {
3437    setTimeout(function() { if ( Xinha.loadPlugins(plugins, callbackIfNotReady) ) { callbackIfNotReady(); } }, 150);
3438  }
3439  return retVal;
3440};
3441
3442//
3443/** Refresh plugin by calling onGenerate or onGenerateOnce method.
3444 * @private
3445 * @param {PluginInstance} plugin
3446 */
3447Xinha.refreshPlugin = function(plugin)
3448{
3449  if ( plugin && typeof plugin.onGenerate == "function" )
3450  {
3451    plugin.onGenerate();
3452  }
3453  if ( plugin && typeof plugin.onGenerateOnce == "function" )
3454  {
3455    plugin.onGenerateOnce();
3456    plugin.onGenerateOnce = null;
3457  }
3458};
3459
3460/** Call a method of all plugins which define the method using the supplied arguments.<br /><br />
3461 *
3462 *  Example: <code>editor.firePluginEvent('onExecCommand', 'paste')</code><br />
3463 *           The plugin would then define a method<br />
3464 *           <code>PluginName.prototype.onExecCommand = function (cmdID, UI, param) {do something...}</code><br /><br />
3465 *           The following methodNames are currently available:<br />
3466 *  <table border="1">
3467 *    <tr>
3468 *       <th>methodName</th><th>Parameters</th>
3469 *     </tr>
3470 *     <tr>
3471 *       <td>onExecCommand</td><td> cmdID, UI, param</td>
3472 *     </tr>
3473 *     <tr>
3474 *       <td>onKeyPress</td><td>ev</td>
3475 *     </tr>
3476 *     <tr>
3477 *       <td>onMouseDown</td><td>ev</td>
3478 *     </tr>
3479 * </table><br /><br />
3480 * 
3481 *  The browser specific plugin (if any) is called last.  The result of each call is
3482 *  treated as boolean.  A true return means that the event will stop, no further plugins
3483 *  will get the event, a false return means the event will continue to fire.
3484 *
3485 *  @param {String} methodName
3486 *  @param {mixed} arguments to pass to the method, optional [2..n]
3487 *  @returns {Boolean}
3488 */
3489
3490Xinha.prototype.firePluginEvent = function(methodName)
3491{
3492  // arguments is not a real array so we can't just .shift() it unfortunatly.
3493  var argsArray = [ ];
3494  for(var i = 1; i < arguments.length; i++)
3495  {
3496    argsArray[i-1] = arguments[i];
3497  }
3498 
3499  for ( var i in this.plugins )
3500  {
3501    var plugin = this.plugins[i].instance;
3502   
3503    // Skip the browser specific plugin
3504    if ( plugin == this._browserSpecificPlugin) continue;
3505   
3506    if ( plugin && typeof plugin[methodName] == "function" )
3507    {
3508      if ( plugin[methodName].apply(plugin, argsArray) )
3509      {
3510        return true;
3511      }
3512    }
3513  }
3514 
3515  // Now the browser speific
3516  var plugin = this._browserSpecificPlugin;
3517  if ( plugin && typeof plugin[methodName] == "function" )
3518  {
3519    if ( plugin[methodName].apply(plugin, argsArray) )
3520    {
3521      return true;
3522    }
3523  }
3524   
3525  return false;
3526}
3527/** Adds a stylesheet to the document
3528 * @param {String} style name of the stylesheet file
3529 * @param {String} plugin optional name of a plugin; if passed this function looks for the stylesheet file in the plugin directory
3530 * @param {String} id optional a unique id for identifiing the created link element, e.g. for avoiding double loading
3531 *                 or later removing it again
3532 */
3533Xinha.loadStyle = function(style, plugin, id,prepend)
3534{
3535  var url = _editor_url || '';
3536  if ( plugin )
3537  {
3538    url = Xinha.getPluginDir( plugin ) + "/";
3539  }
3540  url += style;
3541  // @todo: would not it be better to check the first character instead of a regex ?
3542  // if ( typeof style == 'string' && style.charAt(0) == '/' )
3543  // {
3544  //   url = style;
3545  // }
3546  if ( /^\//.test(style) )
3547  {
3548    url = style;
3549  }
3550  var head = document.getElementsByTagName("head")[0];
3551  var link = document.createElement("link");
3552  link.rel = "stylesheet";
3553  link.href = url;
3554  link.type = "text/css";
3555  if (id) link.id = id;
3556  if (prepend && head.getElementsByTagName('link')[0])
3557  {
3558    head.insertBefore(link,head.getElementsByTagName('link')[0]);
3559  }
3560  else
3561  {
3562    head.appendChild(link);
3563  }
3564 
3565};
3566
3567
3568/***************************************************
3569 *  Category: EDITOR UTILITIES
3570 ***************************************************/
3571/** Utility function: Outputs the structure of the edited document */
3572Xinha.prototype.debugTree = function()
3573{
3574  var ta = document.createElement("textarea");
3575  ta.style.width = "100%";
3576  ta.style.height = "20em";
3577  ta.value = "";
3578  function debug(indent, str)
3579  {
3580    for ( ; --indent >= 0; )
3581    {
3582      ta.value += " ";
3583    }
3584    ta.value += str + "\n";
3585  }
3586  function _dt(root, level)
3587  {
3588    var tag = root.tagName.toLowerCase(), i;
3589    var ns = Xinha.is_ie ? root.scopeName : root.prefix;
3590    debug(level, "- " + tag + " [" + ns + "]");
3591    for ( i = root.firstChild; i; i = i.nextSibling )
3592    {
3593      if ( i.nodeType == 1 )
3594      {
3595        _dt(i, level + 2);
3596      }
3597    }
3598  }
3599  _dt(this._doc.body, 0);
3600  document.body.appendChild(ta);
3601};
3602/** Extracts the textual content of a given node
3603 * @param {DomNode} el
3604 */
3605
3606Xinha.getInnerText = function(el)
3607{
3608  var txt = '', i;
3609  for ( i = el.firstChild; i; i = i.nextSibling )
3610  {
3611    if ( i.nodeType == 3 )
3612    {
3613      txt += i.data;
3614    }
3615    else if ( i.nodeType == 1 )
3616    {
3617      txt += Xinha.getInnerText(i);
3618    }
3619  }
3620  return txt;
3621};
3622/** Cleans dirty HTML from MS word; always cleans the whole editor content
3623 *  @TODO: move this in a separate file
3624 *  @TODO: turn this into a static function that cleans a given string
3625 */
3626Xinha.prototype._wordClean = function()
3627{
3628  var editor = this;
3629  var stats =
3630  {
3631    empty_tags : 0,
3632    mso_class  : 0,
3633    mso_style  : 0,
3634    mso_xmlel  : 0,
3635    orig_len   : this._doc.body.innerHTML.length,
3636    T          : (new Date()).getTime()
3637  };
3638  var stats_txt =
3639  {
3640    empty_tags : "Empty tags removed: ",
3641    mso_class  : "MSO class names removed: ",
3642    mso_style  : "MSO inline style removed: ",
3643    mso_xmlel  : "MSO XML elements stripped: "
3644  };
3645
3646  function showStats()
3647  {
3648    var txt = "Xinha word cleaner stats: \n\n";
3649    for ( var i in stats )
3650    {
3651      if ( stats_txt[i] )
3652      {
3653        txt += stats_txt[i] + stats[i] + "\n";
3654      }
3655    }
3656    txt += "\nInitial document length: " + stats.orig_len + "\n";
3657    txt += "Final document length: " + editor._doc.body.innerHTML.length + "\n";
3658    txt += "Clean-up took " + (((new Date()).getTime() - stats.T) / 1000) + " seconds";
3659    alert(txt);
3660  }
3661
3662  function clearClass(node)
3663  {
3664    var newc = node.className.replace(/(^|\s)mso.*?(\s|$)/ig, ' ');
3665    if ( newc != node.className )
3666    {
3667      node.className = newc;
3668      if ( ! ( /\S/.test(node.className) ) )
3669      {
3670        node.removeAttribute("className");
3671        ++stats.mso_class;
3672      }
3673    }
3674  }
3675
3676  function clearStyle(node)
3677  {
3678    var declarations = node.style.cssText.split(/\s*;\s*/);
3679    for ( var i = declarations.length; --i >= 0; )
3680    {
3681      if ( ( /^mso|^tab-stops/i.test(declarations[i]) ) || ( /^margin\s*:\s*0..\s+0..\s+0../i.test(declarations[i]) ) )
3682      {
3683        ++stats.mso_style;
3684        declarations.splice(i, 1);
3685      }
3686    }
3687    node.style.cssText = declarations.join("; ");
3688  }
3689
3690  var stripTag = null;
3691  if ( Xinha.is_ie )
3692  {
3693    stripTag = function(el)
3694    {
3695      el.outerHTML = Xinha.htmlEncode(el.innerText);
3696      ++stats.mso_xmlel;
3697    };
3698  }
3699  else
3700  {
3701    stripTag = function(el)
3702    {
3703      var txt = document.createTextNode(Xinha.getInnerText(el));
3704      el.parentNode.insertBefore(txt, el);
3705      Xinha.removeFromParent(el);
3706      ++stats.mso_xmlel;
3707    };
3708  }
3709
3710  function checkEmpty(el)
3711  {
3712    // @todo : check if this is quicker
3713    //  if (!['A','SPAN','B','STRONG','I','EM','FONT'].contains(el.tagName) && !el.firstChild)
3714    if ( /^(span|b|strong|i|em|font|div|p)$/i.test(el.tagName) && !el.firstChild)
3715    {
3716      Xinha.removeFromParent(el);
3717      ++stats.empty_tags;
3718    }
3719  }
3720
3721  function parseTree(root)
3722  {
3723    var tag = root.tagName.toLowerCase(), i, next;
3724    // @todo : probably better to use String.indexOf() instead of this ugly regex
3725    // if ((Xinha.is_ie && root.scopeName != 'HTML') || (!Xinha.is_ie && tag.indexOf(':') !== -1)) {
3726    if ( ( Xinha.is_ie && root.scopeName != 'HTML' ) || ( !Xinha.is_ie && ( /:/.test(tag) ) ) )
3727    {
3728      stripTag(root);
3729      return false;
3730    }
3731    else
3732    {
3733      clearClass(root);
3734      clearStyle(root);
3735      for ( i = root.firstChild; i; i = next )
3736      {
3737        next = i.nextSibling;
3738        if ( i.nodeType == 1 && parseTree(i) )
3739        {
3740          checkEmpty(i);
3741        }
3742      }
3743    }
3744    return true;
3745  }
3746  parseTree(this._doc.body);
3747  // showStats();
3748  // this.debugTree();
3749  // this.setHTML(this.getHTML());
3750  // this.setHTML(this.getInnerHTML());
3751  // this.forceRedraw();
3752  this.updateToolbar();
3753};
3754
3755/** Removes &lt;font&gt; tags; always cleans the whole editor content
3756 *  @TODO: move this in a separate file
3757 *  @TODO: turn this into a static function that cleans a given string
3758 */
3759Xinha.prototype._clearFonts = function()
3760{
3761  var D = this.getInnerHTML();
3762
3763  if ( confirm(Xinha._lc("Would you like to clear font typefaces?")) )
3764  {
3765    D = D.replace(/face="[^"]*"/gi, '');
3766    D = D.replace(/font-family:[^;}"']+;?/gi, '');
3767  }
3768
3769  if ( confirm(Xinha._lc("Would you like to clear font sizes?")) )
3770  {
3771    D = D.replace(/size="[^"]*"/gi, '');
3772    D = D.replace(/font-size:[^;}"']+;?/gi, '');
3773  }
3774
3775  if ( confirm(Xinha._lc("Would you like to clear font colours?")) )
3776  {
3777    D = D.replace(/color="[^"]*"/gi, '');
3778    D = D.replace(/([^-])color:[^;}"']+;?/gi, '$1');
3779  }
3780
3781  D = D.replace(/(style|class)="\s*"/gi, '');
3782  D = D.replace(/<(font|span)\s*>/gi, '');
3783  this.setHTML(D);
3784  this.updateToolbar();
3785};
3786
3787Xinha.prototype._splitBlock = function()
3788{
3789  this._doc.execCommand('formatblock', false, 'div');
3790};
3791
3792/** Sometimes the display has to be refreshed to make DOM changes visible (?) (Gecko bug?)  */
3793Xinha.prototype.forceRedraw = function()
3794{
3795  this._doc.body.style.visibility = "hidden";
3796  this._doc.body.style.visibility = "";
3797  // this._doc.body.innerHTML = this.getInnerHTML();
3798};
3799
3800/** Focuses the iframe window.
3801 * @returns {document} a reference to the editor document
3802 */
3803Xinha.prototype.focusEditor = function()
3804{
3805  switch (this._editMode)
3806  {
3807    // notice the try { ... } catch block to avoid some rare exceptions in FireFox
3808    // (perhaps also in other Gecko browsers). Manual focus by user is required in
3809    // case of an error. Somebody has an idea?
3810    case "wysiwyg" :
3811      try
3812      {
3813        // We don't want to focus the field unless at least one field has been activated.
3814        if ( Xinha._someEditorHasBeenActivated )
3815        {
3816          this.activateEditor(); // Ensure *this* editor is activated
3817          this._iframe.contentWindow.focus(); // and focus it
3818        }
3819      } catch (ex) {}
3820    break;
3821    case "textmode":
3822      try
3823      {
3824        this._textArea.focus();
3825      } catch (e) {}
3826    break;
3827    default:
3828      alert("ERROR: mode " + this._editMode + " is not defined");
3829  }
3830  return this._doc;
3831};
3832
3833/** Takes a snapshot of the current text (for undo)
3834 * @private
3835 */
3836Xinha.prototype._undoTakeSnapshot = function()
3837{
3838  ++this._undoPos;
3839  if ( this._undoPos >= this.config.undoSteps )
3840  {
3841    // remove the first element
3842    this._undoQueue.shift();
3843    --this._undoPos;
3844  }
3845  // use the fasted method (getInnerHTML);
3846  var take = true;
3847  var txt = this.getInnerHTML();
3848  if ( this._undoPos > 0 )
3849  {
3850    take = (this._undoQueue[this._undoPos - 1] != txt);
3851  }
3852  if ( take )
3853  {
3854    this._undoQueue[this._undoPos] = txt;
3855  }
3856  else
3857  {
3858    this._undoPos--;
3859  }
3860};
3861/** Custom implementation of undo functionality
3862 * @private
3863 */
3864Xinha.prototype.undo = function()
3865{
3866  if ( this._undoPos > 0 )
3867  {
3868    var txt = this._undoQueue[--this._undoPos];
3869    if ( txt )
3870    {
3871      this.setHTML(txt);
3872    }
3873    else
3874    {
3875      ++this._undoPos;
3876    }
3877  }
3878};
3879/** Custom implementation of redo functionality
3880 * @private
3881 */
3882Xinha.prototype.redo = function()
3883{
3884  if ( this._undoPos < this._undoQueue.length - 1 )
3885  {
3886    var txt = this._undoQueue[++this._undoPos];
3887    if ( txt )
3888    {
3889      this.setHTML(txt);
3890    }
3891    else
3892    {
3893      --this._undoPos;
3894    }
3895  }
3896};
3897/** Disables (greys out) the buttons of the toolbar
3898 * @param {Array} except this array contains ids of toolbar objects that will not be disabled
3899 */
3900Xinha.prototype.disableToolbar = function(except)
3901{
3902  if ( this._timerToolbar )
3903  {
3904    clearTimeout(this._timerToolbar);
3905  }
3906  if ( typeof except == 'undefined' )
3907  {
3908    except = [ ];
3909  }
3910  else if ( typeof except != 'object' )
3911  {
3912    except = [except];
3913  }
3914
3915  for ( var i in this._toolbarObjects )
3916  {
3917    var btn = this._toolbarObjects[i];
3918    if ( except.contains(i) )
3919    {
3920      continue;
3921    }
3922    // prevent iterating over wrong type
3923    if ( typeof(btn.state) != 'function' )
3924    {
3925      continue;
3926    }
3927    btn.state("enabled", false);
3928  }
3929};
3930/** Enables the toolbar again when disabled by disableToolbar() */
3931Xinha.prototype.enableToolbar = function()
3932{
3933  this.updateToolbar();
3934};
3935
3936/** Updates enabled/disable/active state of the toolbar elements, the statusbar and other things
3937 *  This function is called on every key stroke as well as by a timer on a regular basis.<br />
3938 *  Plugins have the opportunity to implement a prototype.onUpdateToolbar() method, which will also
3939 *  be called by this function.
3940 * @param {Boolean} noStatus private use Exempt updating of statusbar
3941 */
3942// FIXME : this function needs to be splitted in more functions.
3943// It is actually to heavy to be understable and very scary to manipulate
3944Xinha.prototype.updateToolbar = function(noStatus)
3945{
3946  var doc = this._doc;
3947  var text = (this._editMode == "textmode");
3948  var ancestors = null;
3949  if ( !text )
3950  {
3951    ancestors = this.getAllAncestors();
3952    if ( this.config.statusBar && !noStatus )
3953    {
3954      while ( this._statusBarItems.length )
3955      {
3956        var item = this._statusBarItems.pop();
3957        item.el = null;
3958        item.editor = null;
3959        item.onclick = null;
3960        item.oncontextmenu = null;
3961        item._xinha_dom0Events['click'] = null;
3962        item._xinha_dom0Events['contextmenu'] = null;
3963        item = null;
3964      }
3965
3966      this._statusBarTree.innerHTML = Xinha._lc("Path") + ": "; // clear
3967      for ( var i = ancestors.length; --i >= 0; )
3968      {
3969        var el = ancestors[i];
3970        if ( !el )
3971        {
3972          // hell knows why we get here; this
3973          // could be a classic example of why
3974          // it's good to check for conditions
3975          // that are impossible to happen ;-)
3976          continue;
3977        }
3978        var a = document.createElement("a");
3979        a.href = "javascript:void(0)";
3980        a.el = el;
3981        a.editor = this;
3982        this._statusBarItems.push(a);
3983        Xinha.addDom0Event(
3984          a,
3985          'click',
3986          function() {
3987            this.blur();
3988            this.editor.selectNodeContents(this.el);
3989            this.editor.updateToolbar(true);
3990            return false;
3991          }
3992        );
3993        Xinha.addDom0Event(
3994          a,
3995          'contextmenu',
3996          function()
3997          {
3998            // TODO: add context menu here
3999            this.blur();
4000            var info = "Inline style:\n\n";
4001            info += this.el.style.cssText.split(/;\s*/).join(";\n");
4002            alert(info);
4003            return false;
4004          }
4005        );
4006        var txt = el.tagName.toLowerCase();
4007        if (typeof el.style != 'undefined') a.title = el.style.cssText;
4008        if ( el.id )
4009        {
4010          txt += "#" + el.id;
4011        }
4012        if ( el.className )
4013        {
4014          txt += "." + el.className;
4015        }
4016        a.appendChild(document.createTextNode(txt));
4017        this._statusBarTree.appendChild(a);
4018        if ( i !== 0 )
4019        {
4020          this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb)));         
4021        }
4022        Xinha.freeLater(a);
4023      }
4024    }
4025  }
4026
4027  for ( var cmd in this._toolbarObjects )
4028  {
4029    var btn = this._toolbarObjects[cmd];
4030    var inContext = true;
4031    // prevent iterating over wrong type
4032    if ( typeof(btn.state) != 'function' )
4033    {
4034      continue;
4035    }
4036    if ( btn.context && !text )
4037    {
4038      inContext = false;
4039      var context = btn.context;
4040      var attrs = [];
4041      if ( /(.*)\[(.*?)\]/.test(context) )
4042      {
4043        context = RegExp.$1;
4044        attrs = RegExp.$2.split(",");
4045      }
4046      context = context.toLowerCase();
4047      var match = (context == "*");
4048      for ( var k = 0; k < ancestors.length; ++k )
4049      {
4050        if ( !ancestors[k] )
4051        {
4052          // the impossible really happens.
4053          continue;
4054        }
4055        if ( match || ( ancestors[k].tagName.toLowerCase() == context ) )
4056        {
4057          inContext = true;
4058          var contextSplit = null;
4059          var att = null;
4060          var comp = null;
4061          var attVal = null;
4062          for ( var ka = 0; ka < attrs.length; ++ka )
4063          {
4064            contextSplit = attrs[ka].match(/(.*)(==|!=|===|!==|>|>=|<|<=)(.*)/);
4065            att = contextSplit[1];
4066            comp = contextSplit[2];
4067            attVal = contextSplit[3];
4068
4069            if (!eval(ancestors[k][att] + comp + attVal))
4070            {
4071              inContext = false;
4072              break;
4073            }
4074          }
4075          if ( inContext )
4076          {
4077            break;
4078          }
4079        }
4080      }
4081    }
4082    btn.state("enabled", (!text || btn.text) && inContext);
4083    if ( typeof cmd == "function" )
4084    {
4085      continue;
4086    }
4087    // look-it-up in the custom dropdown boxes
4088    var dropdown = this.config.customSelects[cmd];
4089    if ( ( !text || btn.text ) && ( typeof dropdown != "undefined" ) )
4090    {
4091      dropdown.refresh(this);
4092      continue;
4093    }
4094    switch (cmd)
4095    {
4096      case "fontname":
4097      case "fontsize":
4098        if ( !text )
4099        {
4100          try
4101          {
4102            var value = ("" + doc.queryCommandValue(cmd)).toLowerCase();
4103            if ( !value )
4104            {
4105              btn.element.selectedIndex = 0;
4106              break;
4107            }
4108
4109            // HACK -- retrieve the config option for this
4110            // combo box.  We rely on the fact that the
4111            // variable in config has the same name as
4112            // button name in the toolbar.
4113            var options = this.config[cmd];
4114            var sIndex = 0;
4115            for ( var j in options )
4116            {
4117            // FIXME: the following line is scary.
4118              if ( ( j.toLowerCase() == value ) || ( options[j].substr(0, value.length).toLowerCase() == value ) )
4119              {
4120                btn.element.selectedIndex = sIndex;
4121                throw "ok";
4122              }
4123              ++sIndex;
4124            }
4125            btn.element.selectedIndex = 0;
4126          } catch(ex) {}
4127        }
4128      break;
4129
4130      // It's better to search for the format block by tag name from the
4131      //  current selection upwards, because IE has a tendancy to return
4132      //  things like 'heading 1' for 'h1', which breaks things if you want
4133      //  to call your heading blocks 'header 1'.  Stupid MS.
4134      case "formatblock":
4135        var blocks = [];
4136        for ( var indexBlock in this.config.formatblock )
4137        {
4138          // prevent iterating over wrong type
4139          if ( typeof this.config.formatblock[indexBlock] == 'string' )
4140          {
4141            blocks[blocks.length] = this.config.formatblock[indexBlock];
4142          }
4143        }
4144
4145        var deepestAncestor = this._getFirstAncestor(this.getSelection(), blocks);
4146        if ( deepestAncestor )
4147        {
4148          for ( var x = 0; x < blocks.length; x++ )
4149          {
4150            if ( blocks[x].toLowerCase() == deepestAncestor.tagName.toLowerCase() )
4151            {
4152              btn.element.selectedIndex = x;
4153            }
4154          }
4155        }
4156        else
4157        {
4158          btn.element.selectedIndex = 0;
4159        }
4160      break;
4161
4162      case "textindicator":
4163        if ( !text )
4164        {
4165          try
4166          {
4167            var style = btn.element.style;
4168            style.backgroundColor = Xinha._makeColor(doc.queryCommandValue(Xinha.is_ie ? "backcolor" : "hilitecolor"));
4169            if ( /transparent/i.test(style.backgroundColor) )
4170            {
4171              // Mozilla
4172              style.backgroundColor = Xinha._makeColor(doc.queryCommandValue("backcolor"));
4173            }
4174            style.color = Xinha._makeColor(doc.queryCommandValue("forecolor"));
4175            style.fontFamily = doc.queryCommandValue("fontname");
4176            style.fontWeight = doc.queryCommandState("bold") ? "bold" : "normal";
4177            style.fontStyle = doc.queryCommandState("italic") ? "italic" : "normal";
4178          } catch (ex) {
4179            // alert(e + "\n\n" + cmd);
4180          }
4181        }
4182      break;
4183
4184      case "htmlmode":
4185        btn.state("active", text);
4186      break;
4187
4188      case "lefttoright":
4189      case "righttoleft":
4190        var eltBlock = this.getParentElement();
4191        while ( eltBlock && !Xinha.isBlockElement(eltBlock) )
4192        {
4193          eltBlock = eltBlock.parentNode;
4194        }
4195        if ( eltBlock )
4196        {
4197          btn.state("active", (eltBlock.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr")));
4198        }
4199      break;
4200
4201      default:
4202        cmd = cmd.replace(/(un)?orderedlist/i, "insert$1orderedlist");
4203        try
4204        {
4205          btn.state("active", (!text && doc.queryCommandState(cmd)));
4206        } catch (ex) {}
4207      break;
4208    }
4209  }
4210  // take undo snapshots
4211  if ( this._customUndo && !this._timerUndo )
4212  {
4213    this._undoTakeSnapshot();
4214    var editor = this;
4215    this._timerUndo = setTimeout(function() { editor._timerUndo = null; }, this.config.undoTimeout);
4216  }
4217  this.firePluginEvent('onUpdateToolbar');
4218};
4219
4220/** Returns a editor object referenced by the id or name of the textarea or the textarea node itself
4221 * For example to retrieve the HTML of an editor made out of the textarea with the id "myTextArea" you would do<br />
4222 * <code>
4223 *       var editor = Xinha.getEditor("myTextArea");
4224 *   var html = editor.getEditorContent();
4225 * </code>
4226 * @returns {Xinha|null}
4227 * @param {String|DomNode} ref id or name of the textarea or the textarea node itself
4228 */
4229Xinha.getEditor = function(ref)
4230{
4231  for ( var i = __xinhas.length; i--; )
4232  {
4233    var editor = __xinhas[i];
4234    if ( editor && ( editor._textArea.id == ref || editor._textArea.name == ref || editor._textArea == ref ) )
4235    {
4236      return editor;
4237    }
4238  }
4239  return null;
4240};
4241/** Sometimes one wants to call a plugin method directly, e.g. from outside the editor.
4242 * This function returns the respective editor's instance of a plugin.
4243 * For example you might want to have a button to trigger SaveSubmit's save() method:<br />
4244 * <code>
4245 *       &lt;button type="button" onclick="Xinha.getEditor('myTextArea').getPluginInstance('SaveSubmit').save();return false;"&gt;Save&lt;/button&gt;
4246 * </code>
4247 * @returns {PluginObject|null}
4248 * @param {String} plugin name of the plugin
4249 */
4250Xinha.prototype.getPluginInstance = function (plugin)
4251{
4252  if (this.plugins[plugin])
4253  {
4254    return this.plugins[plugin].instance;
4255  }
4256  else return null;
4257};
4258/** Returns an array with all the ancestor nodes of the selection or current cursor position.
4259* @returns {Array}
4260*/
4261Xinha.prototype.getAllAncestors = function()
4262{
4263  var p = this.getParentElement();
4264  var a = [];
4265  while ( p && (p.nodeType == 1) && ( p.tagName.toLowerCase() != 'body' ) )
4266  {
4267    a.push(p);
4268    p = p.parentNode;
4269  }
4270  a.push(this._doc.body);
4271  return a;
4272};
4273
4274/** Traverses the DOM upwards and returns the first element that is of one of the specified types
4275 *  @param {Selection} sel  Selection object as returned by getSelection
4276 *  @param {Array} types Array of HTML tag names (lower case)
4277 *  @returns {DomNode|null}
4278 */
4279Xinha.prototype._getFirstAncestor = function(sel, types)
4280{
4281  var prnt = this.activeElement(sel);
4282  if ( prnt === null )
4283  {
4284    // Hmm, I think Xinha.getParentElement() would do the job better?? - James
4285    try
4286    {
4287      prnt = (Xinha.is_ie ? this.createRange(sel).parentElement() : this.createRange(sel).commonAncestorContainer);
4288    }
4289    catch(ex)
4290    {
4291      return null;
4292    }
4293  }
4294
4295  if ( typeof types == 'string' )
4296  {
4297    types = [types];
4298  }
4299
4300  while ( prnt )
4301  {
4302    if ( prnt.nodeType == 1 )
4303    {
4304      if ( types === null )
4305      {
4306        return prnt;
4307      }
4308      if ( types.contains(prnt.tagName.toLowerCase()) )
4309      {
4310        return prnt;
4311      }
4312      if ( prnt.tagName.toLowerCase() == 'body' )
4313      {
4314        break;
4315      }
4316      if ( prnt.tagName.toLowerCase() == 'table' )
4317      {
4318        break;
4319      }
4320    }
4321    prnt = prnt.parentNode;
4322  }
4323
4324  return null;
4325};
4326
4327/** Traverses the DOM upwards and returns the first element that is a block level element
4328 *  @param {Selection} sel  Selection object as returned by getSelection
4329 *  @returns {DomNode|null}
4330 */
4331Xinha.prototype._getAncestorBlock = function(sel)
4332{
4333  // Scan upwards to find a block level element that we can change or apply to
4334  var prnt = (Xinha.is_ie ? this.createRange(sel).parentElement : this.createRange(sel).commonAncestorContainer);
4335
4336  while ( prnt && ( prnt.nodeType == 1 ) )
4337  {
4338    switch ( prnt.tagName.toLowerCase() )
4339    {
4340      case 'div':
4341      case 'p':
4342      case 'address':
4343      case 'blockquote':
4344      case 'center':
4345      case 'del':
4346      case 'ins':
4347      case 'pre':
4348      case 'h1':
4349      case 'h2':
4350      case 'h3':
4351      case 'h4':
4352      case 'h5':
4353      case 'h6':
4354      case 'h7':
4355        // Block Element
4356        return prnt;
4357
4358      case 'body':
4359      case 'noframes':
4360      case 'dd':
4361      case 'li':
4362      case 'th':
4363      case 'td':
4364      case 'noscript' :
4365        // Halting element (stop searching)
4366        return null;
4367
4368      default:
4369        // Keep lookin
4370        break;
4371    }
4372  }
4373
4374  return null;
4375};
4376
4377/** What's this? does nothing, has to be removed
4378 *
4379 * @deprecated
4380 */
4381Xinha.prototype._createImplicitBlock = function(type)
4382{
4383  // expand it until we reach a block element in either direction
4384  // then wrap the selection in a block and return
4385  var sel = this.getSelection();
4386  if ( Xinha.is_ie )
4387  {
4388    sel.empty();
4389  }
4390  else
4391  {
4392    sel.collapseToStart();
4393  }
4394
4395  var rng = this.createRange(sel);
4396
4397  // Expand UP
4398
4399  // Expand DN
4400};
4401
4402
4403
4404/**
4405 *  Call this function to surround the existing HTML code in the selection with
4406 *  your tags.  FIXME: buggy! Don't use this
4407 * @todo: when will it be deprecated ? Can it be removed already ?
4408 * @private (tagged private to not further promote use of this function)
4409 * @deprecated
4410 */
4411Xinha.prototype.surroundHTML = function(startTag, endTag)
4412{
4413  var html = this.getSelectedHTML();
4414  // the following also deletes the selection
4415  this.insertHTML(startTag + html + endTag);
4416};
4417
4418/** Return true if we have some selection
4419 *  @returns {Boolean}
4420 */
4421Xinha.prototype.hasSelectedText = function()
4422{
4423  // FIXME: come _on_ mishoo, you can do better than this ;-)
4424  return this.getSelectedHTML() !== '';
4425};
4426
4427/***************************************************
4428 *  Category: EVENT HANDLERS
4429 ***************************************************/
4430
4431/** onChange handler for dropdowns in toolbar
4432 *  @private
4433 *  @param {DomNode} el Reference to the SELECT object
4434 *  @param {String} txt  The name of the select field, as in config.toolbar
4435 *  @returns {DomNode|null}
4436 */
4437Xinha.prototype._comboSelected = function(el, txt)
4438{
4439  this.focusEditor();
4440  var value = el.options[el.selectedIndex].value;
4441  switch (txt)
4442  {
4443    case "fontname":
4444    case "fontsize":
4445      this.execCommand(txt, false, value);
4446    break;
4447    case "formatblock":
4448      // Mozilla inserts an empty tag (<>) if no parameter is passed 
4449      if ( !value )
4450      {
4451        this.updateToolbar();
4452        break;
4453      }
4454      if( !Xinha.is_gecko || value !== 'blockquote' )
4455      {
4456        value = "<" + value + ">";
4457      }
4458      this.execCommand(txt, false, value);
4459    break;
4460    default:
4461      // try to look it up in the registered dropdowns
4462      var dropdown = this.config.customSelects[txt];
4463      if ( typeof dropdown != "undefined" )
4464      {
4465        dropdown.action(this);
4466      }
4467      else
4468      {
4469        alert("FIXME: combo box " + txt + " not implemented");
4470      }
4471    break;
4472  }
4473};
4474
4475/** Open a popup to select the hilitecolor or forecolor
4476 * @private
4477 * @param {String} cmdID The commande ID (hilitecolor or forecolor)
4478 */
4479Xinha.prototype._colorSelector = function(cmdID)
4480{
4481  var editor = this;    // for nested functions
4482
4483  // backcolor only works with useCSS/styleWithCSS (see mozilla bug #279330 & Midas doc)
4484  // and its also nicer as <font>
4485  if ( Xinha.is_gecko )
4486  {
4487    try
4488    {
4489     editor._doc.execCommand('useCSS', false, false); // useCSS deprecated & replaced by styleWithCSS
4490     editor._doc.execCommand('styleWithCSS', false, true);
4491
4492    } catch (ex) {}
4493  }
4494 
4495  var btn = editor._toolbarObjects[cmdID].element;
4496  var initcolor;
4497  if ( cmdID == 'hilitecolor' )
4498  {
4499    if ( Xinha.is_ie )
4500    {
4501      cmdID = 'backcolor';
4502      initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("backcolor"));
4503    }
4504    else
4505    {
4506      initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("hilitecolor"));
4507    }
4508  }
4509  else
4510  {
4511        initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("forecolor"));
4512  }
4513  var cback = function(color) { editor._doc.execCommand(cmdID, false, color); };
4514  if ( Xinha.is_ie )
4515  {
4516    var range = editor.createRange(editor.getSelection());
4517    cback = function(color)
4518    {
4519      range.select();
4520      editor._doc.execCommand(cmdID, false, color);
4521    };
4522  }
4523  var picker = new Xinha.colorPicker(
4524  {
4525        cellsize:editor.config.colorPickerCellSize,
4526        callback:cback,
4527        granularity:editor.config.colorPickerGranularity,
4528        websafe:editor.config.colorPickerWebSafe,
4529        savecolors:editor.config.colorPickerSaveColors
4530  });
4531  picker.open(editor.config.colorPickerPosition, btn, initcolor);
4532};
4533
4534/** This is a wrapper for the browser's execCommand function that handles things like
4535 *  formatting, inserting elements, etc.<br />
4536 *  It intercepts some commands and replaces them with our own implementation.<br />
4537 *  It provides a hook for the "firePluginEvent" system ("onExecCommand").<br /><br />
4538 *  For reference see:<br />
4539 *     <a href="http://www.mozilla.org/editor/midas-spec.html">Mozilla implementation</a><br />
4540 *     <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/execcommand.asp">MS implementation</a>
4541 *
4542 *  @see Xinha#firePluginEvent
4543 *  @param {String} cmdID command to be executed as defined in the browsers implemantations or Xinha custom
4544 *  @param {Boolean} UI for compatibility with the execCommand syntax; false in most (all) cases
4545 *  @param {Mixed} param Some commands require parameters
4546 *  @returns {Boolean} always false
4547 */
4548Xinha.prototype.execCommand = function(cmdID, UI, param)
4549{
4550  var editor = this;    // for nested functions
4551  this.focusEditor();
4552  cmdID = cmdID.toLowerCase();
4553 
4554  // See if any plugins want to do something special
4555  if(this.firePluginEvent('onExecCommand', cmdID, UI, param))
4556  {
4557    this.updateToolbar();
4558    return false;
4559  }
4560
4561  switch (cmdID)
4562  {
4563    case "htmlmode":
4564      this.setMode();
4565    break;
4566
4567    case "hilitecolor":
4568    case "forecolor":
4569      this._colorSelector(cmdID);
4570    break;
4571
4572    case "createlink":
4573      this._createLink();
4574    break;
4575
4576    case "undo":
4577    case "redo":
4578      if (this._customUndo)
4579      {
4580        this[cmdID]();
4581      }
4582      else
4583      {
4584        this._doc.execCommand(cmdID, UI, param);
4585      }
4586    break;
4587
4588    case "inserttable":
4589      this._insertTable();
4590    break;
4591
4592    case "insertimage":
4593      this._insertImage();
4594    break;
4595
4596    case "about":
4597      this._popupDialog(editor.config.URIs.about, null, this);
4598    break;
4599
4600    case "showhelp":
4601      this._popupDialog(editor.config.URIs.help, null, this);
4602    break;
4603
4604    case "killword":
4605      this._wordClean();
4606    break;
4607
4608    case "cut":
4609    case "copy":
4610    case "paste":
4611      this._doc.execCommand(cmdID, UI, param);
4612      if ( this.config.killWordOnPaste )
4613      {
4614        this._wordClean();
4615      }
4616    break;
4617    case "lefttoright":
4618    case "righttoleft":
4619      if (this.config.changeJustifyWithDirection)
4620      {
4621        this._doc.execCommand((cmdID == "righttoleft") ? "justifyright" : "justifyleft", UI, param);
4622      }
4623      var dir = (cmdID == "righttoleft") ? "rtl" : "ltr";
4624      var el = this.getParentElement();
4625      while ( el && !Xinha.isBlockElement(el) )
4626      {
4627        el = el.parentNode;
4628      }
4629      if ( el )
4630      {
4631        if ( el.style.direction == dir )
4632        {
4633          el.style.direction = "";
4634        }
4635        else
4636        {
4637          el.style.direction = dir;
4638        }
4639      }
4640    break;
4641   
4642    case 'justifyleft'  :
4643    case 'justifyright' :
4644    {
4645      cmdID.match(/^justify(.*)$/);
4646      var ae = this.activeElement(this.getSelection());     
4647      if(ae && ae.tagName.toLowerCase() == 'img')
4648      {
4649        ae.align = ae.align == RegExp.$1 ? '' : RegExp.$1;
4650      }
4651      else
4652      {
4653        this._doc.execCommand(cmdID, UI, param);
4654      }
4655    }   
4656    break;
4657   
4658    default:
4659      try
4660      {
4661        this._doc.execCommand(cmdID, UI, param);
4662      }
4663      catch(ex)
4664      {
4665        if ( this.config.debug )
4666        {
4667          alert(ex + "\n\nby execCommand(" + cmdID + ");");
4668        }
4669      }
4670    break;
4671  }
4672
4673  this.updateToolbar();
4674  return false;
4675};
4676
4677/** A generic event handler for things that happen in the IFRAME's document.<br />
4678 *  It provides two hooks for the "firePluginEvent" system:<br />
4679 *   "onKeyPress"<br />
4680 *   "onMouseDown"
4681 *  @see Xinha#firePluginEvent
4682 *  @param {Event} ev
4683 */
4684Xinha.prototype._editorEvent = function(ev)
4685{
4686  var editor = this;
4687
4688  //call events of textarea
4689  if ( typeof editor._textArea['on'+ev.type] == "function" )
4690  {
4691    editor._textArea['on'+ev.type]();
4692  }
4693 
4694  if ( this.isKeyEvent(ev) )
4695  {
4696    // Run the ordinary plugins first
4697    if(editor.firePluginEvent('onKeyPress', ev))
4698    {
4699      return false;
4700    }
4701   
4702    // Handle the core shortcuts
4703    if ( this.isShortCut( ev ) )
4704    {
4705      this._shortCuts(ev);
4706    }
4707  }
4708
4709  if ( ev.type == 'mousedown' )
4710  {
4711    if(editor.firePluginEvent('onMouseDown', ev))
4712    {
4713      return false;
4714    }
4715  }
4716  // update the toolbar state after some time
4717  if ( editor._timerToolbar )
4718  {
4719    clearTimeout(editor._timerToolbar);
4720  }
4721  editor._timerToolbar = setTimeout(
4722    function()
4723    {
4724      editor.updateToolbar();
4725      editor._timerToolbar = null;
4726    },
4727    250);
4728};
4729
4730/** Handles ctrl + key shortcuts
4731 *  @TODO: make this mor flexible
4732 *  @private
4733 *  @param {Event} ev
4734 */
4735Xinha.prototype._shortCuts = function (ev)
4736{
4737  var key = this.getKey(ev).toLowerCase();
4738  var cmd = null;
4739  var value = null;
4740  switch (key)
4741  {
4742    // simple key commands follow
4743
4744    case 'b': cmd = "bold"; break;
4745    case 'i': cmd = "italic"; break;
4746    case 'u': cmd = "underline"; break;
4747    case 's': cmd = "strikethrough"; break;
4748    case 'l': cmd = "justifyleft"; break;
4749    case 'e': cmd = "justifycenter"; break;
4750    case 'r': cmd = "justifyright"; break;
4751    case 'j': cmd = "justifyfull"; break;
4752    case 'z': cmd = "undo"; break;
4753    case 'y': cmd = "redo"; break;
4754    case 'v': cmd = "paste"; break;
4755    case 'n':
4756    cmd = "formatblock";
4757    value = "p";
4758    break;
4759
4760    case '0': cmd = "killword"; break;
4761
4762    // headings
4763    case '1':
4764    case '2':
4765    case '3':
4766    case '4':
4767    case '5':
4768    case '6':
4769    cmd = "formatblock";
4770    value = "h" + key;
4771    break;
4772  }
4773  if ( cmd )
4774  {
4775    // execute simple command
4776    this.execCommand(cmd, false, value);
4777    Xinha._stopEvent(ev);
4778  }
4779};
4780/** Changes the type of a given node
4781 *  @param {DomNode} el The element to convert
4782 *  @param {String} newTagName The type the element will be converted to
4783 *  @returns {DomNode} A reference to the new element
4784 */
4785Xinha.prototype.convertNode = function(el, newTagName)
4786{
4787  var newel = this._doc.createElement(newTagName);
4788  while ( el.firstChild )
4789  {
4790    newel.appendChild(el.firstChild);
4791  }
4792  return newel;
4793};
4794
4795/** Scrolls the editor iframe to a given element or to the cursor
4796 *  @param {DomNode} e optional The element to scroll to; if ommitted, element the element the cursor is in
4797 */
4798Xinha.prototype.scrollToElement = function(e)
4799{
4800  if(!e)
4801  {
4802    e = this.getParentElement();
4803    if(!e) return;
4804  }
4805 
4806  // This was at one time limited to Gecko only, but I see no reason for it to be. - James
4807  var position = Xinha.getElementTopLeft(e); 
4808  this._iframe.contentWindow.scrollTo(position.left, position.top);
4809};
4810
4811/** Get the edited HTML
4812 * 
4813 *  @public
4814 *  @returns {String} HTML content
4815 */
4816Xinha.prototype.getEditorContent = function()
4817{
4818  return this.outwardHtml(this.getHTML());
4819}
4820
4821/** Completely change the HTML inside the editor
4822 *
4823 *  @public
4824 *  @param {String} html new content
4825 */
4826Xinha.prototype.setEditorContent = function(html)
4827{
4828  this.setHTML(this.inwardHtml(html));
4829}
4830
4831/** Get the raw edited HTML, should not be used without Xinha.prototype.outwardHtml()
4832 * 
4833 *  @private
4834 *  @returns {String} HTML content
4835 */
4836Xinha.prototype.getHTML = function()
4837{
4838  var html = '';
4839  switch ( this._editMode )
4840  {
4841    case "wysiwyg":
4842      if ( !this.config.fullPage )
4843      {
4844        html = Xinha.getHTML(this._doc.body, false, this).trim();
4845      }
4846      else
4847      {
4848        html = this.doctype + "\n" + Xinha.getHTML(this._doc.documentElement, true, this);
4849      }
4850    break;
4851    case "textmode":
4852      html = this._textArea.value;
4853    break;
4854    default:
4855      alert("Mode <" + this._editMode + "> not defined!");
4856      return false;
4857  }
4858  return html;
4859};
4860
4861/** Performs various transformations of the HTML used internally, complement to Xinha.prototype.inwardHtml() 
4862 *  Plugins can provide their own, additional transformations by defining a plugin.prototype.outwardHtml() implematation,
4863 *  which is called by this function
4864 *
4865 *  @private
4866 *  @see Xinha#inwardHtml
4867 *  @param {String} html
4868 *  @returns {String} HTML content
4869 */
4870Xinha.prototype.outwardHtml = function(html)
4871{
4872  for ( var i in this.plugins )
4873  {
4874    var plugin = this.plugins[i].instance;   
4875    if ( plugin && typeof plugin.outwardHtml == "function" )
4876    {
4877      html = plugin.outwardHtml(html);
4878    }
4879  }
4880 
4881  html = html.replace(/<(\/?)b(\s|>|\/)/ig, "<$1strong$2");
4882  html = html.replace(/<(\/?)i(\s|>|\/)/ig, "<$1em$2");
4883  html = html.replace(/<(\/?)strike(\s|>|\/)/ig, "<$1del$2");
4884 
4885  // remove disabling of inline event handle inside Xinha iframe
4886  html = html.replace(/(<[^>]*on(click|mouse(over|out|up|down))=['"])if\(window\.parent &amp;&amp; window\.parent\.Xinha\)\{return false\}/gi,'$1');
4887
4888  // Figure out what our server name is, and how it's referenced
4889  var serverBase = location.href.replace(/(https?:\/\/[^\/]*)\/.*/, '$1') + '/';
4890
4891  // IE puts this in can't figure out why
4892  //  leaving this in the core instead of InternetExplorer
4893  //  because it might be something we are doing so could present itself
4894  //  in other browsers - James
4895  html = html.replace(/https?:\/\/null\//g, serverBase);
4896
4897  // Make semi-absolute links to be truely absolute
4898  //  we do this just to standardize so that special replacements knows what
4899  //  to expect
4900  html = html.replace(/((href|src|background)=[\'\"])\/+/ig, '$1' + serverBase);
4901
4902  html = this.outwardSpecialReplacements(html);
4903
4904  html = this.fixRelativeLinks(html);
4905
4906  if ( this.config.sevenBitClean )
4907  {
4908    html = html.replace(/[^ -~\r\n\t]/g, function(c) { return '&#'+c.charCodeAt(0)+';'; });
4909  }
4910 
4911  //prevent execution of JavaScript (Ticket #685)
4912  html = html.replace(/(<script[^>]*((type=[\"\']text\/)|(language=[\"\'])))(freezescript)/gi,"$1javascript");
4913
4914  // If in fullPage mode, strip the coreCSS
4915  if(this.config.fullPage)
4916  {
4917    html = Xinha.stripCoreCSS(html);
4918  }
4919
4920  if (typeof this.config.outwardHtml == 'function' )
4921  {
4922    html = this.config.outwardHtml(html);
4923  }
4924
4925  return html;
4926};
4927
4928/** Performs various transformations of the HTML to be edited
4929 *  Plugins can provide their own, additional transformations by defining a plugin.prototype.inwardHtml() implematation,
4930 *  which is called by this function
4931 * 
4932 *  @private
4933 *  @see Xinha#outwardHtml
4934 *  @param {String} html 
4935 *  @returns {String} transformed HTML
4936 */
4937Xinha.prototype.inwardHtml = function(html)
4938
4939  for ( var i in this.plugins )
4940  {
4941    var plugin = this.plugins[i].instance;   
4942    if ( plugin && typeof plugin.inwardHtml == "function" )
4943    {
4944      html = plugin.inwardHtml(html);
4945    }   
4946  }
4947   
4948  // Both IE and Gecko use strike instead of del (#523)
4949  html = html.replace(/<(\/?)del(\s|>|\/)/ig, "<$1strike$2");
4950
4951  // disable inline event handle inside Xinha iframe
4952  html = html.replace(/(<[^>]*on(click|mouse(over|out|up|down))=["'])/gi,'$1if(window.parent &amp;&amp; window.parent.Xinha){return false}');
4953 
4954  html = this.inwardSpecialReplacements(html);
4955
4956  html = html.replace(/(<script[^>]*((type=[\"\']text\/)|(language=[\"\'])))(javascript)/gi,"$1freezescript");
4957
4958  // For IE's sake, make any URLs that are semi-absolute (="/....") to be
4959  // truely absolute
4960  var nullRE = new RegExp('((href|src|background)=[\'"])/+', 'gi');
4961  html = html.replace(nullRE, '$1' + location.href.replace(/(https?:\/\/[^\/]*)\/.*/, '$1') + '/');
4962
4963  html = this.fixRelativeLinks(html);
4964 
4965  // If in fullPage mode, add the coreCSS
4966  if(this.config.fullPage)
4967  {
4968    html = Xinha.addCoreCSS(html);
4969  }
4970
4971  if (typeof this.config.inwardHtml == 'function' )
4972  {
4973    html = this.config.inwardHtml(html);
4974  }
4975
4976  return html;
4977};
4978/** Apply the replacements defined in Xinha.Config.specialReplacements
4979 * 
4980 *  @private
4981 *  @see Xinha#inwardSpecialReplacements
4982 *  @param {String} html
4983 *  @returns {String}  transformed HTML
4984 */
4985Xinha.prototype.outwardSpecialReplacements = function(html)
4986{
4987  for ( var i in this.config.specialReplacements )
4988  {
4989    var from = this.config.specialReplacements[i];
4990    var to   = i; // why are declaring a new variable here ? Seems to be better to just do : for (var to in config)
4991    // prevent iterating over wrong type
4992    if ( typeof from.replace != 'function' || typeof to.replace != 'function' )
4993    {
4994      continue;
4995    }
4996    // alert('out : ' + from + '=>' + to);
4997    var reg = new RegExp(Xinha.escapeStringForRegExp(from), 'g');
4998    html = html.replace(reg, to.replace(/\$/g, '$$$$'));
4999    //html = html.replace(from, to);
5000  }
5001  return html;
5002};
5003/** Apply the replacements defined in Xinha.Config.specialReplacements
5004 * 
5005 *  @private
5006 *  @see Xinha#outwardSpecialReplacements
5007 *  @param {String} html
5008 *  @returns {String}  transformed HTML
5009 */
5010Xinha.prototype.inwardSpecialReplacements = function(html)
5011{
5012  // alert("inward");
5013  for ( var i in this.config.specialReplacements )
5014  {
5015    var from = i; // why are declaring a new variable here ? Seems to be better to just do : for (var from in config)
5016    var to   = this.config.specialReplacements[i];
5017    // prevent iterating over wrong type
5018    if ( typeof from.replace != 'function' || typeof to.replace != 'function' )
5019    {
5020      continue;
5021    }
5022    // alert('in : ' + from + '=>' + to);
5023    //
5024    // html = html.replace(reg, to);
5025    // html = html.replace(from, to);
5026    var reg = new RegExp(Xinha.escapeStringForRegExp(from), 'g');
5027    html = html.replace(reg, to.replace(/\$/g, '$$$$')); // IE uses doubled dollar signs to escape backrefs, also beware that IE also implements $& $_ and $' like perl.
5028  }
5029  return html;
5030};
5031/** Transforms the paths in src & href attributes
5032 * 
5033 *  @private
5034 *  @see Xinha.Config#expandRelativeUrl
5035 *  @see Xinha.Config#stripSelfNamedAnchors
5036 *  @see Xinha.Config#stripBaseHref
5037 *  @see Xinha.Config#baseHref
5038 *  @param {String} html
5039 *  @returns {String} transformed HTML
5040 */
5041Xinha.prototype.fixRelativeLinks = function(html)
5042{
5043  if ( typeof this.config.expandRelativeUrl != 'undefined' && this.config.expandRelativeUrl )
5044  var src = html.match(/(src|href)="([^"]*)"/gi);
5045  var b = document.location.href;
5046  if ( src )
5047  {
5048    var url,url_m,relPath,base_m,absPath
5049    for ( var i=0;i<src.length;++i )
5050    {
5051      url = src[i].match(/(src|href)="([^"]*)"/i);
5052      url_m = url[2].match( /\.\.\//g );
5053      if ( url_m )
5054      {
5055        relPath = new RegExp( "(.*?)(([^\/]*\/){"+ url_m.length+"})[^\/]*$" );
5056        base_m = b.match( relPath );
5057        absPath = url[2].replace(/(\.\.\/)*/,base_m[1]);
5058        html = html.replace( new RegExp(Xinha.escapeStringForRegExp(url[2])),absPath );
5059      }
5060    }
5061  }
5062 
5063  if ( typeof this.config.stripSelfNamedAnchors != 'undefined' && this.config.stripSelfNamedAnchors )
5064  {
5065    var stripRe = new RegExp("((href|src|background)=\")("+Xinha.escapeStringForRegExp(unescape(document.location.href.replace(/&/g,'&amp;'))) + ')([#?][^\'" ]*)', 'g');
5066    html = html.replace(stripRe, '$1$4');
5067  }
5068
5069  if ( typeof this.config.stripBaseHref != 'undefined' && this.config.stripBaseHref )
5070  {
5071    var baseRe = null;
5072    if ( typeof this.config.baseHref != 'undefined' && this.config.baseHref !== null )
5073    {
5074      baseRe = new RegExp( "((href|src|background|action)=\")(" + Xinha.escapeStringForRegExp(this.config.baseHref.replace(/([^\/]\/)(?=.+\.)[^\/]*$/, "$1")) + ")", 'g' );
5075          html = html.replace(baseRe, '$1');
5076    }
5077    baseRe = new RegExp( "((href|src|background|action)=\")(" +  Xinha.escapeStringForRegExp(document.location.href.replace( /^(https?:\/\/[^\/]*)(.*)/, '$1' )) + ")", 'g' );
5078    html = html.replace(baseRe, '$1');
5079  }
5080
5081  return html;
5082};
5083
5084/** retrieve the HTML (fastest version, but uses innerHTML)
5085 * 
5086 *  @private
5087 *  @returns {String} HTML content
5088 */
5089Xinha.prototype.getInnerHTML = function()
5090{
5091  if ( !this._doc.body )
5092  {
5093    return '';
5094  }
5095  var html = "";
5096  switch ( this._editMode )
5097  {
5098    case "wysiwyg":
5099      if ( !this.config.fullPage )
5100      {
5101        // return this._doc.body.innerHTML;
5102        html = this._doc.body.innerHTML;
5103      }
5104      else
5105      {
5106        html = this.doctype + "\n" + this._doc.documentElement.innerHTML;
5107      }
5108    break;
5109    case "textmode" :
5110      html = this._textArea.value;
5111    break;
5112    default:
5113      alert("Mode <" + this._editMode + "> not defined!");
5114      return false;
5115  }
5116
5117  return html;
5118};
5119
5120/** Completely change the HTML inside
5121 *
5122 *  @private
5123 *  @param {String} html new content, should have been run through inwardHtml() first
5124 */
5125Xinha.prototype.setHTML = function(html)
5126{
5127  if ( !this.config.fullPage )
5128  {
5129    this._doc.body.innerHTML = html;
5130  }
5131  else
5132  {
5133    this.setFullHTML(html);
5134  }
5135  this._textArea.value = html;
5136};
5137
5138/** sets the given doctype (useful only when config.fullPage is true)
5139 * 
5140 *  @private
5141 *  @param {String} doctype
5142 */
5143Xinha.prototype.setDoctype = function(doctype)
5144{
5145  this.doctype = doctype;
5146};
5147
5148/***************************************************
5149 *  Category: UTILITY FUNCTIONS
5150 ***************************************************/
5151
5152/** Variable used to pass the object to the popup editor window.
5153 *  @FIXME: Is this in use?
5154 *  @deprecated
5155 *  @private
5156 *  @type {Object}
5157 */
5158Xinha._object = null;
5159
5160/** Arrays are identified as "object" in typeof calls. Adding this tag to the Array prototype allows to distinguish between the two
5161 */
5162Array.prototype.isArray = true;
5163/** RegExps are identified as "object" in typeof calls. Adding this tag to the RegExp prototype allows to distinguish between the two
5164 */
5165RegExp.prototype.isRegExp = true;
5166/** function that returns a clone of the given object
5167 * 
5168 *  @private
5169 *  @param {Object} obj
5170 *  @returns {Object} cloned object
5171 */
5172Xinha.cloneObject = function(obj)
5173{
5174  if ( !obj )
5175  {
5176    return null;
5177  }
5178  var newObj = (obj.isArray ) ? [] : {};
5179
5180  // check for function and RegExp objects (as usual, IE is fucked up)
5181  if ( obj.constructor.toString().match( /\s*function Function\(/ ) || typeof obj == 'function' )
5182  {
5183    newObj = obj; // just copy reference to it
5184  }
5185  else if (  obj.isRegExp )
5186  {
5187    newObj = eval( obj.toString() ); //see no way without eval
5188  }
5189  else
5190  {
5191    for ( var n in obj )
5192    {
5193      var node = obj[n];
5194      if ( typeof node == 'object' )
5195      {
5196        newObj[n] = Xinha.cloneObject(node);
5197      }
5198      else
5199      {
5200        newObj[n] = node;
5201      }
5202    }
5203  }
5204
5205  return newObj;
5206};
5207
5208/** Event Flushing
5209 *  To try and work around memory leaks in the rather broken
5210 *  garbage collector in IE, Xinha.flushEvents can be called
5211 *  onunload, it will remove any event listeners (that were added
5212 *  through _addEvent(s)) and clear any DOM-0 events.
5213 *  @private
5214 *
5215 */
5216Xinha.flushEvents = function()
5217{
5218  var x = 0;
5219  // @todo : check if Array.prototype.pop exists for every supported browsers
5220  var e = Xinha._eventFlushers.pop();
5221  while ( e )
5222  {
5223    try
5224    {
5225      if ( e.length == 3 )
5226      {
5227        Xinha._removeEvent(e[0], e[1], e[2]);
5228        x++;
5229      }
5230      else if ( e.length == 2 )
5231      {
5232        e[0]['on' + e[1]] = null;
5233        e[0]._xinha_dom0Events[e[1]] = null;
5234        x++;
5235      }
5236    }
5237    catch(ex)
5238    {
5239      // Do Nothing
5240    }
5241    e = Xinha._eventFlushers.pop();
5242  }
5243 
5244  /*
5245    // This code is very agressive, and incredibly slow in IE, so I've disabled it.
5246   
5247    if(document.all)
5248    {
5249      for(var i = 0; i < document.all.length; i++)
5250      {
5251        for(var j in document.all[i])
5252        {
5253          if(/^on/.test(j) && typeof document.all[i][j] == 'function')
5254          {
5255            document.all[i][j] = null;
5256            x++;
5257          }
5258        }
5259      }
5260    }
5261  */
5262 
5263  // alert('Flushed ' + x + ' events.');
5264};
5265 /** Holds the events to be flushed
5266  * @type Array
5267  */
5268Xinha._eventFlushers = [];
5269
5270if ( document.addEventListener )
5271{
5272 /** adds an event listener for the specified element and event type
5273 * 
5274 *  @public
5275 *  @see   Xinha#_addEvents
5276 *  @see   Xinha#addDom0Event
5277 *  @see   Xinha#prependDom0Event
5278 *  @param {DomNode}  el the DOM element the event should be attached to
5279 *  @param {String}   evname the name of the event to listen for (without leading "on")
5280 *  @param {function} func the function to be called when the event is fired
5281 */
5282  Xinha._addEvent = function(el, evname, func)
5283  {
5284    el.addEventListener(evname, func, true);
5285    Xinha._eventFlushers.push([el, evname, func]);
5286  };
5287 
5288 /** removes an event listener previously added
5289 * 
5290 *  @public
5291 *  @see   Xinha#_removeEvents
5292 *  @param {DomNode}  el the DOM element the event should be removed from
5293 *  @param {String}   evname the name of the event the listener should be removed from (without leading "on")
5294 *  @param {function} func the function to be removed
5295 */
5296  Xinha._removeEvent = function(el, evname, func)
5297  {
5298    el.removeEventListener(evname, func, true);
5299  };
5300 
5301 /** stops bubbling of the event, if no further listeners should be triggered
5302 * 
5303 *  @public
5304 *  @param {event} ev the event to be stopped
5305 */
5306  Xinha._stopEvent = function(ev)
5307  {
5308    ev.preventDefault();
5309    ev.stopPropagation();
5310  };
5311}
5312 /** same as above, for IE
5313 * 
5314 */
5315else if ( document.attachEvent )
5316{
5317  Xinha._addEvent = function(el, evname, func)
5318  {
5319    el.attachEvent("on" + evname, func);
5320    Xinha._eventFlushers.push([el, evname, func]);
5321  };
5322  Xinha._removeEvent = function(el, evname, func)
5323  {
5324    el.detachEvent("on" + evname, func);
5325  };
5326  Xinha._stopEvent = function(ev)
5327  {
5328    try
5329    {
5330      ev.cancelBubble = true;
5331      ev.returnValue = false;
5332    }
5333    catch (ex)
5334    {
5335      // Perhaps we could try here to stop the window.event
5336      // window.event.cancelBubble = true;
5337      // window.event.returnValue = false;
5338    }
5339  };
5340}
5341else
5342{
5343  Xinha._addEvent = function(el, evname, func)
5344  {
5345    alert('_addEvent is not supported');
5346  };
5347  Xinha._removeEvent = function(el, evname, func)
5348  {
5349    alert('_removeEvent is not supported');
5350  };
5351  Xinha._stopEvent = function(ev)
5352  {
5353    alert('_stopEvent is not supported');
5354  };
5355}
5356 /** add several events at once to one element
5357 * 
5358 *  @public
5359 *  @see Xinha#_addEvent
5360 *  @param {DomNode}  el the DOM element the event should be attached to
5361 *  @param {Array}    evs the names of the event to listen for (without leading "on")
5362 *  @param {function} func the function to be called when the event is fired
5363 */
5364Xinha._addEvents = function(el, evs, func)
5365{
5366  for ( var i = evs.length; --i >= 0; )
5367  {
5368    Xinha._addEvent(el, evs[i], func);
5369  }
5370};
5371 /** remove several events at once to from element
5372 * 
5373 *  @public
5374 *  @see Xinha#_removeEvent
5375 *  @param {DomNode}  el the DOM element the events should be remove from
5376 *  @param {Array}    evs the names of the events the listener should be removed from (without leading "on")
5377 *  @param {function} func the function to be removed
5378 */
5379Xinha._removeEvents = function(el, evs, func)
5380{
5381  for ( var i = evs.length; --i >= 0; )
5382  {
5383    Xinha._removeEvent(el, evs[i], func);
5384  }
5385};
5386
5387/** Adds a function that is executed in the moment the DOM is ready, but as opposed to window.onload before images etc. have been loaded
5388*   http://dean.edwards.name/weblog/2006/06/again/
5389*  @public
5390*  @author Dean Edwards/Matthias Miller/ John Resig / Raimund Meyer
5391*  @param {Function}  func the function to be executed
5392*  @param {Window}    scope the window that is listened to
5393*/
5394Xinha.addOnloadHandler = function (func, scope)
5395{
5396 scope = scope ? scope : window;
5397
5398 var init = function ()
5399 {
5400   // quit if this function has already been called
5401   if (arguments.callee.done) return;
5402   // flag this function so we don't do the same thing twice
5403   arguments.callee.done = true;
5404   // kill the timer
5405   if (Xinha.onloadTimer) clearInterval(Xinha.onloadTimer);
5406
5407   func();
5408 }
5409 if (Xinha.is_ie)
5410 {
5411   scope.document.write("<sc"+"ript id=__ie_onload defer src=javascript:void(0)><\/script>");
5412   var script = scope.document.getElementById("__ie_onload");
5413      script.onreadystatechange = function()
5414   {
5415     if (this.readyState == "loaded") // We want this as early as possible, so I changed 'complete' to 'loaded', because otherwise it fired even after window.onload
5416     {
5417                this.parentNode.removeChild(script);
5418       init(); // call the onload handler
5419     }
5420   };
5421 }
5422 else if (/applewebkit|KHTML/i.test(navigator.userAgent) ) /* Safari/WebKit/KHTML */
5423 {
5424   Xinha.onloadTimer = scope.setInterval(function()
5425   {
5426     if (/loaded|complete/.test(scope.document.readyState))
5427     {
5428       init(); // call the onload handler
5429     }
5430   }, 10);
5431 }
5432 else /* for Mozilla/Opera9 */
5433 {
5434   scope.document.addEventListener("DOMContentLoaded", init, false);
5435
5436 }
5437 Xinha._addEvent(scope, 'load', init); // incase anything went wrong
5438};
5439
5440/**
5441 * Adds a standard "DOM-0" event listener to an element.
5442 * The DOM-0 events are those applied directly as attributes to
5443 * an element - eg element.onclick = stuff;
5444 *
5445 * By using this function instead of simply overwriting any existing
5446 * DOM-0 event by the same name on the element it will trigger as well
5447 * as the existing ones.  Handlers are triggered one after the other
5448 * in the order they are added.
5449 *
5450 * Remember to return true/false from your handler, this will determine
5451 * whether subsequent handlers will be triggered (ie that the event will
5452 * continue or be canceled).
5453 * 
5454 *  @public
5455 *  @see Xinha#_addEvent
5456 *  @see Xinha#prependDom0Event
5457 *  @param {DomNode}  el the DOM element the event should be attached to
5458 *  @param {String}   ev the name of the event to listen for (without leading "on")
5459 *  @param {function} fn the function to be called when the event is fired
5460 */
5461
5462Xinha.addDom0Event = function(el, ev, fn)
5463{
5464  Xinha._prepareForDom0Events(el, ev);
5465  el._xinha_dom0Events[ev].unshift(fn);
5466};
5467
5468
5469/** See addDom0Event, the difference is that handlers registered using
5470 *  prependDom0Event will be triggered before existing DOM-0 events of the
5471 *  same name on the same element.
5472 * 
5473 *  @public
5474 *  @see Xinha#_addEvent
5475 *  @see Xinha#addDom0Event
5476 *  @param {DomNode}  the DOM element the event should be attached to
5477 *  @param {String}   the name of the event to listen for (without leading "on")
5478 *  @param {function} the function to be called when the event is fired
5479 */
5480
5481Xinha.prependDom0Event = function(el, ev, fn)
5482{
5483  Xinha._prepareForDom0Events(el, ev);
5484  el._xinha_dom0Events[ev].push(fn);
5485};
5486
5487/**
5488 * Prepares an element to receive more than one DOM-0 event handler
5489 * when handlers are added via addDom0Event and prependDom0Event.
5490 *
5491 * @private
5492 */
5493Xinha._prepareForDom0Events = function(el, ev)
5494{
5495  // Create a structure to hold our lists of event handlers
5496  if ( typeof el._xinha_dom0Events == 'undefined' )
5497  {
5498    el._xinha_dom0Events = {};
5499    Xinha.freeLater(el, '_xinha_dom0Events');
5500  }
5501
5502  // Create a list of handlers for this event type
5503  if ( typeof el._xinha_dom0Events[ev] == 'undefined' )
5504  {
5505    el._xinha_dom0Events[ev] = [ ];
5506    if ( typeof el['on'+ev] == 'function' )
5507    {
5508      el._xinha_dom0Events[ev].push(el['on'+ev]);
5509    }
5510
5511    // Make the actual event handler, which runs through
5512    // each of the handlers in the list and executes them
5513    // in the correct context.
5514    el['on'+ev] = function(event)
5515    {
5516      var a = el._xinha_dom0Events[ev];
5517      // call previous submit methods if they were there.
5518      var allOK = true;
5519      for ( var i = a.length; --i >= 0; )
5520      {
5521        // We want the handler to be a member of the form, not the array, so that "this" will work correctly
5522        el._xinha_tempEventHandler = a[i];
5523        if ( el._xinha_tempEventHandler(event) === false )
5524        {
5525          el._xinha_tempEventHandler = null;
5526          allOK = false;
5527          break;
5528        }
5529        el._xinha_tempEventHandler = null;
5530      }
5531      return allOK;
5532    };
5533
5534    Xinha._eventFlushers.push([el, ev]);
5535  }
5536};
5537
5538Xinha.prototype.notifyOn = function(ev, fn)
5539{
5540  if ( typeof this._notifyListeners[ev] == 'undefined' )
5541  {
5542    this._notifyListeners[ev] = [];
5543    Xinha.freeLater(this, '_notifyListeners');
5544  }
5545  this._notifyListeners[ev].push(fn);
5546};
5547
5548Xinha.prototype.notifyOf = function(ev, args)
5549{
5550  if ( this._notifyListeners[ev] )
5551  {
5552    for ( var i = 0; i < this._notifyListeners[ev].length; i++ )
5553    {
5554      this._notifyListeners[ev][i](ev, args);
5555    }
5556  }
5557};
5558
5559/** List of tag names that are defined as block level elements in HTML
5560 * 
5561 *  @private
5562 *  @see Xinha#isBlockElement
5563 *  @type {String}
5564 */
5565Xinha._blockTags = " body form textarea fieldset ul ol dl li div " +
5566"p h1 h2 h3 h4 h5 h6 quote pre table thead " +
5567"tbody tfoot tr td th iframe address blockquote ";
5568
5569/** Checks if one element is in the list of elements that are defined as block level elements in HTML
5570 * 
5571 *  @param {DomNode}  el The DOM element to check
5572 *  @returns {Boolean}
5573 */
5574Xinha.isBlockElement = function(el)
5575{
5576  return el && el.nodeType == 1 && (Xinha._blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
5577};
5578/** List of tag names that are allowed to contain a paragraph
5579 * 
5580 *  @private
5581 *  @see Xinha#isParaContainer
5582 *  @type {String}
5583 */
5584Xinha._paraContainerTags = " body td th caption fieldset div";
5585/** Checks if one element is in the list of elements that are allowed to contain a paragraph in HTML
5586 * 
5587 *  @param {DomNode}  el The DOM element to check
5588 *  @returns {Boolean}
5589 */
5590Xinha.isParaContainer = function(el)
5591{
5592  return el && el.nodeType == 1 && (Xinha._paraContainerTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
5593};
5594
5595
5596/* * These are all the tags for which the end tag is not optional or  forbidden, taken from the list at:
5597 *   http: www.w3.org/TR/REC-html40/index/elements.html
5598 * 
5599 *  @private
5600 *  @see Xinha#needsClosingTag
5601 *  @type {String}
5602 */
5603Xinha._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 ";
5604
5605/** Checks if one element is in the list of elements for which the end tag is not optional or  forbidden in HTML
5606 * 
5607 *  @param {DomNode}  el The DOM element to check
5608 *  @returns {Boolean}
5609 */
5610Xinha.needsClosingTag = function(el)
5611{
5612  return el && el.nodeType == 1 && (Xinha._closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
5613};
5614
5615/** Performs HTML encoding of some given string (converts HTML special characters to entities)
5616 * 
5617 *  @param {String}  str The unencoded input
5618 *  @returns {String} The encoded output
5619 */
5620Xinha.htmlEncode = function(str)
5621{
5622  if ( typeof str.replace == 'undefined' )
5623  {
5624    str = str.toString();
5625  }
5626  // we don't need regexp for that, but.. so be it for now.
5627  str = str.replace(/&/ig, "&amp;");
5628  str = str.replace(/</ig, "&lt;");
5629  str = str.replace(/>/ig, "&gt;");
5630  str = str.replace(/\xA0/g, "&nbsp;"); // Decimal 160, non-breaking-space
5631  str = str.replace(/\x22/g, "&quot;");
5632  // \x22 means '"' -- we use hex reprezentation so that we don't disturb
5633  // JS compressors (well, at least mine fails.. ;)
5634  return str;
5635};
5636
5637/** Strips host-part of URL which is added by browsers to links relative to server root
5638 * 
5639 *  @param {String}  string
5640 *  @returns {String}
5641 */
5642Xinha.prototype.stripBaseURL = function(string)
5643{
5644  if ( this.config.baseHref === null || !this.config.stripBaseHref )
5645  {
5646    return string;
5647  }
5648  var baseurl = this.config.baseHref.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1');
5649  var basere = new RegExp(baseurl);
5650  return string.replace(basere, "");
5651};
5652/** Removes whitespace from beginning and end of a string
5653 * 
5654 *  @returns {String}
5655 */
5656String.prototype.trim = function()
5657{
5658  return this.replace(/^\s+/, '').replace(/\s+$/, '');
5659};
5660
5661/** Creates a rgb-style rgb(r,g,b) color from a (24bit) number
5662 * 
5663 *  @param {Integer}
5664 *  @returns {String} rgb(r,g,b) color definition
5665 */
5666Xinha._makeColor = function(v)
5667{
5668  if ( typeof v != "number" )
5669  {
5670    // already in rgb (hopefully); IE doesn't get here.
5671    return v;
5672  }
5673  // IE sends number; convert to rgb.
5674  var r = v & 0xFF;
5675  var g = (v >> 8) & 0xFF;
5676  var b = (v >> 16) & 0xFF;
5677  return "rgb(" + r + "," + g + "," + b + ")";
5678};
5679
5680/** Returns hexadecimal color representation from a number or a rgb-style color.
5681 * 
5682 *  @param {String|Integer} v rgb(r,g,b) or 24bit color definition
5683 *  @returns {String} #RRGGBB color definition
5684 */
5685Xinha._colorToRgb = function(v)
5686{
5687  if ( !v )
5688  {
5689    return '';
5690  }
5691  var r,g,b;
5692  // @todo: why declaring this function here ? This needs to be a public methode of the object Xinha._colorToRgb
5693  // returns the hex representation of one byte (2 digits)
5694  function hex(d)
5695  {
5696    return (d < 16) ? ("0" + d.toString(16)) : d.toString(16);
5697  }
5698
5699  if ( typeof v == "number" )
5700  {
5701    // we're talking to IE here
5702    r = v & 0xFF;
5703    g = (v >> 8) & 0xFF;
5704    b = (v >> 16) & 0xFF;
5705    return "#" + hex(r) + hex(g) + hex(b);
5706  }
5707
5708  if ( v.substr(0, 3) == "rgb" )
5709  {
5710    // in rgb(...) form -- Mozilla
5711    var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/;
5712    if ( v.match(re) )
5713    {
5714      r = parseInt(RegExp.$1, 10);
5715      g = parseInt(RegExp.$2, 10);
5716      b = parseInt(RegExp.$3, 10);
5717      return "#" + hex(r) + hex(g) + hex(b);
5718    }
5719    // doesn't match RE?!  maybe uses percentages or float numbers
5720    // -- FIXME: not yet implemented.
5721    return null;
5722  }
5723
5724  if ( v.substr(0, 1) == "#" )
5725  {
5726    // already hex rgb (hopefully :D )
5727    return v;
5728  }
5729
5730  // if everything else fails ;)
5731  return null;
5732};
5733
5734/** Modal popup dialogs
5735 * 
5736 *  @param {String} url URL to the popup dialog
5737 *  @param {Function} action A function that receives one value; this function will get called
5738 *                    after the dialog is closed, with the return value of the dialog.
5739 *  @param {Mixed} init A variable that is passed to the popup window to pass arbitrary data
5740 */
5741Xinha.prototype._popupDialog = function(url, action, init)
5742{
5743  Dialog(this.popupURL(url), action, init);
5744};
5745
5746/** Creates a path in the form _editor_url + "plugins/" + plugin + "/img/" + file
5747 * 
5748 *  @deprecated
5749 *  @param {String} file Name of the image
5750 *  @param {String} plugin optional If omitted, simply _editor_url + file is returned
5751 *  @returns {String}
5752 */
5753Xinha.prototype.imgURL = function(file, plugin)
5754{
5755  if ( typeof plugin == "undefined" )
5756  {
5757    return _editor_url + file;
5758  }
5759  else
5760  {
5761    return _editor_url + "plugins/" + plugin + "/img/" + file;
5762  }
5763};
5764/** Creates a path
5765 * 
5766 *  @deprecated
5767 *  @param {String} file Name of the popup
5768 *  @returns {String}
5769 */
5770Xinha.prototype.popupURL = function(file)
5771{
5772  var url = "";
5773  if ( file.match(/^plugin:\/\/(.*?)\/(.*)/) )
5774  {
5775    var plugin = RegExp.$1;
5776    var popup = RegExp.$2;
5777    if ( ! ( /\.(html?|php)$/.test(popup) ) )
5778    {
5779      popup += ".html";
5780    }
5781    url = Xinha.getPluginDir(plugin) + "/popups/" + popup;
5782  }
5783  else if ( file.match(/^\/.*?/) || file.match(/^https?:\/\//))
5784  {
5785    url = file;
5786  }
5787  else
5788  {
5789    url = _editor_url + this.config.popupURL + file;
5790  }
5791  return url;
5792};
5793
5794
5795
5796/** FIX: Internet Explorer returns an item having the _name_ equal to the given
5797 * id, even if it's not having any id.  This way it can return a different form
5798 * field, even if it's not a textarea.  This workarounds the problem by
5799 * specifically looking to search only elements having a certain tag name.
5800 * @param {String} tag The tag name to limit the return to
5801 * @param {String} id
5802 * @returns {DomNode}
5803 */
5804Xinha.getElementById = function(tag, id)
5805{
5806  var el, i, objs = document.getElementsByTagName(tag);
5807  for ( i = objs.length; --i >= 0 && (el = objs[i]); )
5808  {
5809    if ( el.id == id )
5810    {
5811      return el;
5812    }
5813  }
5814  return null;
5815};
5816
5817
5818/** Use some CSS trickery to toggle borders on tables
5819 *      @returns {Boolean} always true
5820 */
5821
5822Xinha.prototype._toggleBorders = function()
5823{
5824  var tables = this._doc.getElementsByTagName('TABLE');
5825  if ( tables.length !== 0 )
5826  {
5827   if ( !this.borders )
5828   {   
5829    this.borders = true;
5830   }
5831   else
5832   {
5833     this.borders = false;
5834   }
5835
5836   for ( var i=0; i < tables.length; i++ )
5837   {
5838     if ( this.borders )
5839     {
5840        Xinha._addClass(tables[i], 'htmtableborders');
5841     }
5842     else
5843     {
5844       Xinha._removeClass(tables[i], 'htmtableborders');
5845     }
5846   }
5847  }
5848  return true;
5849};
5850/** Adds the styles for table borders to the iframe during generation
5851 * 
5852 *  @private
5853 *  @see Xinha#stripCoreCSS
5854 *  @param {String} html optional 
5855 *  @returns {String} html HTML with added styles or only styles if html omitted
5856 */
5857Xinha.addCoreCSS = function(html)
5858{
5859    var coreCSS =
5860    "<style title=\"XinhaInternalCSS\" type=\"text/css\">"
5861    + ".htmtableborders, .htmtableborders td, .htmtableborders th {border : 1px dashed lightgrey ! important;}\n"
5862    + "html, body { border: 0px; } \n"
5863    + "body { background-color: #ffffff; } \n"
5864        + "img, hr { cursor: default } \n" 
5865    +"</style>\n";
5866   
5867    if( html && /<head>/i.test(html))
5868    {
5869      return html.replace(/<head>/i, '<head>' + coreCSS);     
5870    }
5871    else if ( html)
5872    {
5873      return coreCSS + html;
5874    }
5875    else
5876    {
5877      return coreCSS;
5878    }
5879}
5880/** Allows plugins to add a stylesheet for internal use to the edited document that won't appear in the HTML output
5881 * 
5882 *  @see Xinha#stripCoreCSS
5883 *  @param {String} stylesheet URL of the styleshett to be added
5884 */
5885Xinha.prototype.addEditorStylesheet = function (stylesheet)
5886{
5887    var style = this._doc.createElement("link");
5888    style.rel = 'stylesheet';
5889    style.type = 'text/css';
5890    style.title = 'XinhaInternalCSS';
5891        style.href = stylesheet;
5892    this._doc.getElementsByTagName("HEAD")[0].appendChild(style);
5893}
5894/** Remove internal styles
5895 * 
5896 *  @private
5897 *  @see Xinha#addCoreCSS
5898 *  @param {String} html
5899 *  @returns {String}
5900 */
5901Xinha.stripCoreCSS = function(html)
5902{
5903  return html.replace(/<style[^>]+title="XinhaInternalCSS"(.|\n)*?<\/style>/ig, '').replace(/<link[^>]+title="XinhaInternalCSS"(.|\n)*?>/ig, '');
5904}
5905/** Removes one CSS class (that is one of possible more parts
5906 *   separated by spaces) from a given element
5907 * 
5908 *  @see Xinha#_removeClasses
5909 *  @param {DomNode}  el The DOM element the class will be removed from
5910 *  @param {String}   className The class to be removed
5911 */
5912Xinha._removeClass = function(el, className)
5913{
5914  if ( ! ( el && el.className ) )
5915  {
5916    return;
5917  }
5918  var cls = el.className.split(" ");
5919  var ar = [];
5920  for ( var i = cls.length; i > 0; )
5921  {
5922    if ( cls[--i] != className )
5923    {
5924      ar[ar.length] = cls[i];
5925    }
5926  }
5927  el.className = ar.join(" ");
5928};
5929/** Adds one CSS class  to a given element (that is, it expands its className property by the given string,
5930 *  separated by a space)
5931 * 
5932 *  @see Xinha#addClasses
5933 *  @param {DomNode}  el The DOM element the class will be added to
5934 *  @param {String}   className The class to be added
5935 */
5936Xinha._addClass = function(el, className)
5937{
5938  // remove the class first, if already there
5939  Xinha._removeClass(el, className);
5940  el.className += " " + className;
5941};
5942
5943/** Adds CSS classes  to a given element (that is, it expands its className property by the given string,
5944 *  separated by a space, thereby checking that no class is doubly added)
5945 * 
5946 *  @see Xinha#addClass
5947 *  @param {DomNode}  el The DOM element the classes will be added to
5948 *  @param {String}   classes The classes to be added
5949 */
5950Xinha.addClasses = function(el, classes)
5951{
5952  if ( el !== null )
5953  {
5954    var thiers = el.className.trim().split(' ');
5955    var ours   = classes.split(' ');
5956    for ( var x = 0; x < ours.length; x++ )
5957    {
5958      var exists = false;
5959      for ( var i = 0; exists === false && i < thiers.length; i++ )
5960      {
5961        if ( thiers[i] == ours[x] )
5962        {
5963          exists = true;
5964        }
5965      }
5966      if ( exists === false )
5967      {
5968        thiers[thiers.length] = ours[x];
5969      }
5970    }
5971    el.className = thiers.join(' ').trim();
5972  }
5973};
5974
5975/** Removes CSS classes (that is one or more of possibly several parts
5976 *   separated by spaces) from a given element
5977 * 
5978 *  @see Xinha#_removeClasses
5979 *  @param {DomNode}  el The DOM element the class will be removed from
5980 *  @param {String}   className The class to be removed
5981 */
5982Xinha.removeClasses = function(el, classes)
5983{
5984  var existing    = el.className.trim().split();
5985  var new_classes = [];
5986  var remove      = classes.trim().split();
5987
5988  for ( var i = 0; i < existing.length; i++ )
5989  {
5990    var found = false;
5991    for ( var x = 0; x < remove.length && !found; x++ )
5992    {
5993      if ( existing[i] == remove[x] )
5994      {
5995        found = true;
5996      }
5997    }
5998    if ( !found )
5999    {
6000      new_classes[new_classes.length] = existing[i];
6001    }
6002  }
6003  return new_classes.join(' ');
6004};
6005
6006/** Alias of Xinha._addClass()
6007 *  @see Xinha#_addClass
6008 */
6009Xinha.addClass       = Xinha._addClass;
6010/** Alias of Xinha.Xinha._removeClass()
6011 *  @see Xinha#_removeClass
6012 */
6013Xinha.removeClass    = Xinha._removeClass;
6014/** Alias of Xinha.addClasses()
6015 *  @see Xinha#addClasses
6016 */
6017Xinha._addClasses    = Xinha.addClasses;
6018/** Alias of Xinha.removeClasses()
6019 *  @see Xinha#removeClasses
6020 */
6021Xinha._removeClasses = Xinha.removeClasses;
6022
6023/** Checks if one element has set the given className
6024 * 
6025 *  @param {DomNode}  el The DOM element to check
6026 *  @param {String}   className The class to be looked for
6027 *  @returns {Boolean}
6028 */
6029Xinha._hasClass = function(el, className)
6030{
6031  if ( ! ( el && el.className ) )
6032  {
6033    return false;
6034  }
6035  var cls = el.className.split(" ");
6036  for ( var i = cls.length; i > 0; )
6037  {
6038    if ( cls[--i] == className )
6039    {
6040      return true;
6041    }
6042  }
6043  return false;
6044};
6045
6046/** Use XMLHTTPRequest to post some data back to the server and do something
6047 *  with the response (asyncronously!), this is used by such things as the tidy functions
6048 *  @param {String} url The address for the HTTPRequest
6049 *  @param {Object} data The data to be passed to the server like {name:"value"}
6050 *  @param {Function} handler A function that is called when an answer is received from the server with the responseText
6051 *                             as argument                             
6052 */
6053 
6054// mod_security (an apache module which scans incoming requests for potential hack attempts)
6055// has a rule which triggers when it gets an incoming Content-Type with a charset
6056// see ticket:1028 to try and work around this, if we get a failure in a postback
6057// then Xinha._postback_send_charset will be set to false and the request tried again (once)
6058Xinha._postback_send_charset = true;
6059Xinha._postback = function(url, data, handler)
6060{
6061  var req = null;
6062  req = Xinha.getXMLHTTPRequestObject();
6063
6064  var content = '';
6065  if (typeof data == 'string')
6066  {
6067    content = data;
6068  }
6069  else if(typeof data == "object")
6070  {
6071    for ( var i in data )
6072    {
6073      content += (content.length ? '&' : '') + i + '=' + encodeURIComponent(data[i]);
6074    }
6075  }
6076
6077  function callBack()
6078  {
6079    if ( req.readyState == 4 )
6080    {
6081      if ( req.status == 200 || Xinha.isRunLocally && req.status == 0 )
6082      {
6083        if ( typeof handler == 'function' )
6084        {
6085          handler(req.responseText, req);
6086        }
6087      }
6088      else if(Xinha._postback_send_charset)
6089      {       
6090        Xinha._postback_send_charset = false;
6091        Xinha._postback(url,data,handler);
6092      }
6093      else
6094      {
6095        alert('An error has occurred: ' + req.statusText + '\nURL: ' + url);
6096      }
6097    }
6098  }
6099
6100  req.onreadystatechange = callBack;
6101
6102  req.open('POST', url, true);
6103  req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'+(Xinha._postback_send_charset ? '; charset=UTF-8' : ''));
6104
6105  //alert(content);
6106  req.send(content);
6107};
6108
6109/** Use XMLHTTPRequest to receive some data from the server and do something
6110 *  with the it (asyncronously!)
6111 *  @param {String} url The address for the HTTPRequest
6112 *  @param {Function} handler A function that is called when an answer is received from the server with the responseText
6113 *                             as argument                             
6114 */
6115Xinha._getback = function(url, handler)
6116{
6117  var req = null;
6118  req = Xinha.getXMLHTTPRequestObject();
6119
6120  function callBack()
6121  {
6122    if ( req.readyState == 4 )
6123    {
6124      if ( req.status == 200 || Xinha.isRunLocally && req.status == 0 )
6125      {
6126        handler(req.responseText, req);
6127      }
6128      else
6129      {
6130        alert('An error has occurred: ' + req.statusText + '\nURL: ' + url);
6131      }
6132    }
6133  }
6134
6135  req.onreadystatechange = callBack;
6136  req.open('GET', url, true);
6137  req.send(null);
6138};
6139/** Use XMLHTTPRequest to receive some data from the server syncronously
6140 *  @param {String} url The address for the HTTPRequest
6141 */
6142Xinha._geturlcontent = function(url)
6143{
6144  var req = null;
6145  req = Xinha.getXMLHTTPRequestObject();
6146
6147  // Synchronous!
6148  req.open('GET', url, false);
6149  req.send(null);
6150  if ( req.status == 200 || Xinha.isRunLocally && req.status == 0 )
6151  {
6152    return req.responseText;
6153  }
6154  else
6155  {
6156    return '';
6157  }
6158
6159};
6160
6161// Unless somebody already has, make a little function to debug things
6162
6163if (typeof dumpValues == 'undefined')
6164{
6165  function dumpValues(o)
6166  {
6167    var s = '';
6168    for (var prop in o)
6169    {
6170      if (window.console && typeof window.console.log == 'function')
6171      {
6172        if (typeof console.firebug != 'undefined')
6173          console.log(o);
6174        else
6175          console.log(prop + ' = ' + o[prop] + '\n');
6176      }
6177      else
6178      {
6179        s += prop + ' = ' + o[prop] + '\n';
6180      }
6181
6182    }
6183    if (s)
6184    {
6185      if (document.getElementById('errors'))
6186      {
6187        document.getElementById('errors').value += s;
6188      }
6189      else
6190      {
6191        var x = window.open("", "debugger");
6192        x.document.write('<pre>' + s + '</pre>');
6193      }
6194
6195    }
6196  }
6197}
6198if ( !Array.prototype.contains )
6199{
6200  /** Walks through an array and checks if the specified item exists in it
6201  * @param {String} needle The string to search for
6202  * @returns {Boolean} True if item found, false otherwise
6203  */
6204  Array.prototype.contains = function(needle)
6205  {
6206    var haystack = this;
6207    for ( var i = 0; i < haystack.length; i++ )
6208    {
6209      if ( needle == haystack[i] )
6210      {
6211        return true;
6212      }
6213    }
6214    return false;
6215  };
6216}
6217
6218if ( !Array.prototype.indexOf )
6219{
6220  /** Walks through an array and, if the specified item exists in it, returns the position
6221  * @param {String} needle The string to search for
6222  * @returns {Integer|null} Index position if item found, null otherwise
6223  */
6224  Array.prototype.indexOf = function(needle)
6225  {
6226    var haystack = this;
6227    for ( var i = 0; i < haystack.length; i++ )
6228    {
6229      if ( needle == haystack[i] )
6230      {
6231        return i;
6232      }
6233    }
6234    return null;
6235  };
6236}
6237if ( !Array.prototype.append )
6238{
6239  /** Adds an item to an array
6240   * @param {Mixed} a Item to add
6241   * @returns {Array} The array including the newly added item
6242   */
6243  Array.prototype.append  = function(a)
6244  {
6245    for ( var i = 0; i < a.length; i++ )
6246    {
6247      this.push(a[i]);
6248    }
6249    return this;
6250  };
6251}
6252/** Returns true if all elements of <em>a2</em> are also contained in <em>a1</em> (at least I think this is what it does)
6253* @param {Array} a1
6254* @param {Array} a2
6255* @returns {Boolean}
6256*/
6257Xinha.arrayContainsArray = function(a1, a2)
6258{
6259  var all_found = true;
6260  for ( var x = 0; x < a2.length; x++ )
6261  {
6262    var found = false;
6263    for ( var i = 0; i < a1.length; i++ )
6264    {
6265      if ( a1[i] == a2[x] )
6266      {
6267        found = true;
6268        break;
6269      }
6270    }
6271    if ( !found )
6272    {
6273      all_found = false;
6274      break;
6275    }
6276  }
6277  return all_found;
6278};
6279/** Walks through an array and applies a filter function to each item
6280* @param {Array} a1 The array to filter
6281* @param {Function} filterfn If this function returns true, the item is added to the new array
6282* @returns {Array} Filtered array
6283*/
6284Xinha.arrayFilter = function(a1, filterfn)
6285{
6286  var new_a = [ ];
6287  for ( var x = 0; x < a1.length; x++ )
6288  {
6289    if ( filterfn(a1[x]) )
6290    {
6291      new_a[new_a.length] = a1[x];
6292    }
6293  }
6294  return new_a;
6295};
6296/** Converts a Collection object to an array
6297* @param {Collection} collection The array to filter
6298* @returns {Array} Array containing the item of collection
6299*/
6300Xinha.collectionToArray = function(collection)
6301{
6302  var array = [ ];
6303  for ( var i = 0; i < collection.length; i++ )
6304  {
6305    array.push(collection.item(i));
6306  }
6307  return array;
6308};
6309
6310/** Index for Xinha.uniq function
6311*       @private
6312*/
6313Xinha.uniq_count = 0;
6314/** Returns a string that is unique on the page
6315*       @param {String} prefix This string is prefixed to a running number
6316*   @returns {String}
6317*/
6318Xinha.uniq = function(prefix)
6319{
6320  return prefix + Xinha.uniq_count++;
6321};
6322
6323// New language handling functions
6324
6325/** Load a language file.
6326 *  This function should not be used directly, Xinha._lc will use it when necessary.
6327 *  @private
6328 *  @param {String} context Case sensitive context name, eg 'Xinha', 'TableOperations', ...
6329 *  @returns {Object}
6330 */
6331Xinha._loadlang = function(context,url)
6332{
6333  var lang;
6334 
6335  if ( typeof _editor_lcbackend == "string" )
6336  {
6337    //use backend
6338    url = _editor_lcbackend;
6339    url = url.replace(/%lang%/, _editor_lang);
6340    url = url.replace(/%context%/, context);
6341  }
6342  else if (!url)
6343  {
6344    //use internal files
6345    if ( context != 'Xinha')
6346    {
6347      url = Xinha.getPluginDir(context)+"/lang/"+_editor_lang+".js";
6348    }
6349    else
6350    {
6351      Xinha.setLoadingMessage("Loading language");
6352      url = _editor_url+"lang/"+_editor_lang+".js";
6353    }
6354  }
6355
6356  var langData = Xinha._geturlcontent(url);
6357  if ( langData !== "" )
6358  {
6359    try
6360    {
6361      eval('lang = ' + langData);
6362    }
6363    catch(ex)
6364    {
6365      alert('Error reading Language-File ('+url+'):\n'+Error.toString());
6366      lang = {};
6367    }
6368  }
6369  else
6370  {
6371    lang = {};
6372  }
6373
6374  return lang;
6375};
6376
6377/** Return a localised string.
6378 * @param {String} string English language string. It can also contain variables in the form "Some text with $variable=replaced text$".
6379 *                  This replaces $variable in "Some text with $variable" with "replaced text"
6380 * @param {String} context   Case sensitive context name, eg 'Xinha' (default), 'TableOperations'...
6381 * @param {Object} replace   Replace $variables in String, eg {foo: 'replaceText'} ($foo in string will be replaced by replaceText)
6382 */
6383Xinha._lc = function(string, context, replace)
6384{
6385  var url,ret;
6386  if (typeof context == 'object' && context.url && context.context)
6387  {
6388    url = context.url + _editor_lang + ".js";
6389    context = context.context;
6390  }
6391
6392  var m = null;
6393  if (typeof string == 'string') m = string.match(/\$(.*?)=(.*?)\$/g);
6394  if (m)
6395  {
6396    if (!replace) replace = {};
6397    for (var i = 0;i<m.length;i++)
6398    {
6399      var n = m[i].match(/\$(.*?)=(.*?)\$/);
6400      replace[n[1]] = n[2];
6401      string = string.replace(n[0],'$'+n[1]);
6402    }
6403  }
6404  if ( _editor_lang == "en" )
6405  {
6406    if ( typeof string == 'object' && string.string )
6407    {
6408      ret = string.string;
6409    }
6410    else
6411    {
6412      ret = string;
6413    }
6414  }
6415  else
6416  {
6417    if ( typeof Xinha._lc_catalog == 'undefined' )
6418    {
6419      Xinha._lc_catalog = [ ];
6420    }
6421
6422    if ( typeof context == 'undefined' )
6423    {
6424      context = 'Xinha';
6425    }
6426
6427    if ( typeof Xinha._lc_catalog[context] == 'undefined' )
6428    {
6429      Xinha._lc_catalog[context] = Xinha._loadlang(context,url);
6430    }
6431
6432    var key;
6433    if ( typeof string == 'object' && string.key )
6434    {
6435      key = string.key;
6436    }
6437    else if ( typeof string == 'object' && string.string )
6438    {
6439      key = string.string;
6440    }
6441    else
6442    {
6443      key = string;
6444    }
6445
6446    if ( typeof Xinha._lc_catalog[context][key] == 'undefined' )
6447    {
6448      if ( context=='Xinha' )
6449      {
6450        // Indicate it's untranslated
6451        if ( typeof string == 'object' && string.string )
6452        {
6453          ret = string.string;
6454        }
6455        else
6456        {
6457          ret = string;
6458        }
6459      }
6460      else
6461      {
6462        //if string is not found and context is not Xinha try if it is in Xinha
6463        return Xinha._lc(string, 'Xinha', replace);
6464      }
6465    }
6466    else
6467    {
6468      ret = Xinha._lc_catalog[context][key];
6469    }
6470  }
6471
6472  if ( typeof string == 'object' && string.replace )
6473  {
6474    replace = string.replace;
6475  }
6476  if ( typeof replace != "undefined" )
6477  {
6478    for ( var i in replace )
6479    {
6480      ret = ret.replace('$'+i, replace[i]);
6481    }
6482  }
6483
6484  return ret;
6485};
6486/** Walks through the children of a given element and checks if any of the are visible (= not display:none)
6487 * @param {DomNode} el
6488 * @returns {Boolean}
6489 */
6490Xinha.hasDisplayedChildren = function(el)
6491{
6492  var children = el.childNodes;
6493  for ( var i = 0; i < children.length; i++ )
6494  {
6495    if ( children[i].tagName )
6496    {
6497      if ( children[i].style.display != 'none' )
6498      {
6499        return true;
6500      }
6501    }
6502  }
6503  return false;
6504};
6505
6506/** Load a javascript file by inserting it in the HEAD tag and eventually call a function when loaded
6507 *
6508 *  Note that this method cannot be abstracted into browser specific files
6509 *  because this method LOADS the browser specific files.  Hopefully it should work for most
6510 *  browsers as it is.
6511 *
6512 * @param {String} url               Source url of the file to load
6513 * @param {Object} callback optional Callback function to launch once ready
6514 * @param {Object} scope    optional Application scope for the callback function
6515 * @param {Object} bonus    optional Arbitrary object send as a param to the callback function
6516 */
6517Xinha._loadback = function(url, callback, scope, bonus)
6518
6519  if ( document.getElementById(url) )
6520  {
6521    return true;
6522  }
6523  var t = !Xinha.is_ie ? "onload" : 'onreadystatechange';
6524  var s = document.createElement("script");
6525  s.type = "text/javascript";
6526  s.src = url;
6527  s.id = url;
6528  if ( callback )
6529  {
6530    s[t] = function()
6531    {     
6532      if ( Xinha.is_ie && ( ! ( /loaded|complete/.test(window.event.srcElement.readyState) ) ) )
6533      {
6534        return;
6535      }
6536     
6537      callback.call(scope ? scope : this, bonus);
6538      s[t] = null;
6539    };
6540  }
6541  document.getElementsByTagName("head")[0].appendChild(s);
6542  return false;
6543};
6544
6545/** Xinha's main loading function (see NewbieGuide)
6546 * @param {Array} editor_names
6547 * @param {Xinha.Config} default_config
6548 * @param {Array} plugin_names
6549 * @returns {Object} An object that contains references to all created editors indexed by the IDs of the textareas
6550 */
6551Xinha.makeEditors = function(editor_names, default_config, plugin_names)
6552{
6553  if ( !Xinha.isSupportedBrowser ) return;
6554 
6555  if ( typeof default_config == 'function' )
6556  {
6557    default_config = default_config();
6558  }
6559
6560  var editors = {};
6561  var textarea;
6562  for ( var x = 0; x < editor_names.length; x++ )
6563  {
6564    if ( typeof editor_names[x] == 'string' ) // the regular case, an id of a textarea
6565    {
6566      textarea = Xinha.getElementById('textarea', editor_names[x] );
6567      if (!textarea) // the id may be specified for a textarea that is maybe on another page; we simply skip it and go on
6568      {
6569        editor_names[x] = null;
6570        continue;
6571      }
6572    }
6573         // make it possible to pass a reference instead of an id, for example from  document.getElementsByTagName('textarea')
6574    else if ( typeof editor_names[x] == 'object' && editor_names[x].tagName && editor_names[x].tagName.toLowerCase() == 'textarea' )
6575    {
6576      textarea =  editor_names[x];
6577      if ( !textarea.id ) // we'd like to have the textarea have an id
6578      {
6579        textarea.id = 'xinha_id_' + x;
6580      }
6581    }
6582    var editor = new Xinha(textarea, Xinha.cloneObject(default_config));
6583    editor.registerPlugins(plugin_names);
6584    editors[textarea.id] = editor;
6585  }
6586  return editors;
6587};
6588/** Another main loading function (see NewbieGuide)
6589 * @param {Object} editors As returned by Xinha.makeEditors()
6590 */
6591Xinha.startEditors = function(editors)
6592{
6593  if ( !Xinha.isSupportedBrowser ) return;
6594 
6595  for ( var i in editors )
6596  {
6597    if ( editors[i].generate )
6598    {
6599      editors[i].generate();
6600    }
6601  }
6602};
6603/** Registers the loaded plugins with the editor
6604 * @private
6605 * @param {Array} plugin_names
6606 */
6607Xinha.prototype.registerPlugins = function(plugin_names)
6608{
6609  if ( !Xinha.isSupportedBrowser ) return;
6610 
6611  if ( plugin_names )
6612  {
6613    for ( var i = 0; i < plugin_names.length; i++ )
6614    {
6615      this.setLoadingMessage(Xinha._lc('Register plugin $plugin', 'Xinha', {'plugin': plugin_names[i]}));
6616      this.registerPlugin(plugin_names[i]);
6617    }
6618  }
6619};
6620
6621/** Utility function to base64_encode some arbitrary data, uses the builtin btoa() if it exists (Moz)
6622*  @param {String} input
6623*  @returns {String}
6624*/
6625Xinha.base64_encode = function(input)
6626{
6627  var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
6628  var output = "";
6629  var chr1, chr2, chr3;
6630  var enc1, enc2, enc3, enc4;
6631  var i = 0;
6632
6633  do
6634  {
6635    chr1 = input.charCodeAt(i++);
6636    chr2 = input.charCodeAt(i++);
6637    chr3 = input.charCodeAt(i++);
6638
6639    enc1 = chr1 >> 2;
6640    enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
6641    enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
6642    enc4 = chr3 & 63;
6643
6644    if ( isNaN(chr2) )
6645    {
6646      enc3 = enc4 = 64;
6647    }
6648    else if ( isNaN(chr3) )
6649    {
6650      enc4 = 64;
6651    }
6652
6653    output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
6654  } while ( i < input.length );
6655
6656  return output;
6657};
6658
6659/** Utility function to base64_decode some arbitrary data, uses the builtin atob() if it exists (Moz)
6660 *  @param {String} input
6661 *  @returns {String}
6662 */
6663Xinha.base64_decode = function(input)
6664{
6665  var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
6666  var output = "";
6667  var chr1, chr2, chr3;
6668  var enc1, enc2, enc3, enc4;
6669  var i = 0;
6670
6671  // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
6672  input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
6673
6674  do
6675  {
6676    enc1 = keyStr.indexOf(input.charAt(i++));
6677    enc2 = keyStr.indexOf(input.charAt(i++));
6678    enc3 = keyStr.indexOf(input.charAt(i++));
6679    enc4 = keyStr.indexOf(input.charAt(i++));
6680
6681    chr1 = (enc1 << 2) | (enc2 >> 4);
6682    chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
6683    chr3 = ((enc3 & 3) << 6) | enc4;
6684
6685    output = output + String.fromCharCode(chr1);
6686
6687    if ( enc3 != 64 )
6688    {
6689      output = output + String.fromCharCode(chr2);
6690    }
6691    if ( enc4 != 64 )
6692    {
6693      output = output + String.fromCharCode(chr3);
6694    }
6695  } while ( i < input.length );
6696
6697  return output;
6698};
6699/** Removes a node from the DOM
6700 *  @param {DomNode} el The element to be removed
6701 *  @returns {DomNode} The removed element
6702 */
6703Xinha.removeFromParent = function(el)
6704{
6705  if ( !el.parentNode )
6706  {
6707    return;
6708  }
6709  var pN = el.parentNode;
6710  pN.removeChild(el);
6711  return el;
6712};
6713/** Checks if some element has a parent node
6714 *  @param {DomNode} el
6715 *  @returns {Boolean}
6716 */
6717Xinha.hasParentNode = function(el)
6718{
6719  if ( el.parentNode )
6720  {
6721    // When you remove an element from the parent in IE it makes the parent
6722    // of the element a document fragment.  Moz doesn't.
6723    if ( el.parentNode.nodeType == 11 )
6724    {
6725      return false;
6726    }
6727    return true;
6728  }
6729
6730  return false;
6731};
6732
6733/** Detect the size of visible area
6734 *  @param {Window} scope optional When calling from a popup window, pass its window object to get the values of the popup
6735 *  @returns {Object} Object with Integer properties x and y
6736 */
6737Xinha.viewportSize = function(scope)
6738{
6739  scope = (scope) ? scope : window;
6740  var x,y;
6741  if (scope.innerHeight) // all except Explorer
6742  {
6743    x = scope.innerWidth;
6744    y = scope.innerHeight;
6745  }
6746  else if (scope.document.documentElement && scope.document.documentElement.clientHeight)
6747  // Explorer 6 Strict Mode
6748  {
6749    x = scope.document.documentElement.clientWidth;
6750    y = scope.document.documentElement.clientHeight;
6751  }
6752  else if (scope.document.body) // other Explorers
6753  {
6754    x = scope.document.body.clientWidth;
6755    y = scope.document.body.clientHeight;
6756  }
6757  return {'x':x,'y':y};
6758};
6759/** Detect the size of the whole document
6760 *  @param {Window} scope optional When calling from a popup window, pass its window object to get the values of the popup
6761 *  @returns {Object} Object with Integer properties x and y
6762 */
6763Xinha.pageSize = function(scope)
6764{
6765  scope = (scope) ? scope : window;
6766  var x,y;
6767 
6768  var test1 = scope.document.body.scrollHeight; //IE Quirks
6769  var test2 = scope.document.documentElement.scrollHeight; // IE Standard + Moz Here quirksmode.org errs!
6770
6771  if (test1 > test2)
6772  {
6773    x = scope.document.body.scrollWidth;
6774    y = scope.document.body.scrollHeight;
6775  }
6776  else
6777  {
6778    x = scope.document.documentElement.scrollWidth;
6779    y = scope.document.documentElement.scrollHeight;
6780  } 
6781  return {'x':x,'y':y};
6782};
6783/** Detect the current scroll position
6784 *  @param {Window} scope optional When calling from a popup window, pass its window object to get the values of the popup
6785 *  @returns {Object} Object with Integer properties x and y
6786 */
6787Xinha.prototype.scrollPos = function(scope)
6788{
6789  scope = (scope) ? scope : window;
6790  var x,y;
6791  if (scope.pageYOffset) // all except Explorer
6792  {
6793    x = scope.pageXOffset;
6794    y = scope.pageYOffset;
6795  }
6796  else if (scope.document.documentElement && document.documentElement.scrollTop)
6797    // Explorer 6 Strict
6798  {
6799    x = scope.document.documentElement.scrollLeft;
6800    y = scope.document.documentElement.scrollTop;
6801  }
6802  else if (scope.document.body) // all other Explorers
6803  {
6804    x = scope.document.body.scrollLeft;
6805    y = scope.document.body.scrollTop;
6806  }
6807  return {'x':x,'y':y};
6808};
6809
6810/** Calculate the top and left pixel position of an element in the DOM.
6811 *  @param  {DomNode} element HTML Element
6812 *  @returns {Object} Object with Integer properties top and left
6813 */
6814 
6815Xinha.getElementTopLeft = function(element)
6816{
6817  var curleft = curtop = 0;
6818  if (element.offsetParent)
6819  {
6820    curleft = element.offsetLeft
6821    curtop = element.offsetTop
6822    while (element = element.offsetParent)
6823    {
6824      curleft += element.offsetLeft
6825      curtop += element.offsetTop
6826    }
6827  }
6828  return { top:curtop, left:curleft };
6829}
6830/** Find left pixel position of an element in the DOM.
6831 *  @param  {DomNode} element HTML Element
6832 *  @returns {Integer}
6833 */
6834Xinha.findPosX = function(obj)
6835{
6836  var curleft = 0;
6837  if ( obj.offsetParent )
6838  {
6839    return Xinha.getElementTopLeft(obj).left;   
6840  }
6841  else if ( obj.x )
6842  {
6843    curleft += obj.x;
6844  }
6845  return curleft;
6846};
6847/** Find top pixel position of an element in the DOM.
6848 *  @param  {DomNode} element HTML Element
6849 *  @returns {Integer}
6850 */
6851Xinha.findPosY = function(obj)
6852{
6853  var curtop = 0;
6854  if ( obj.offsetParent )
6855  {
6856    return Xinha.getElementTopLeft(obj).top;   
6857  }
6858  else if ( obj.y )
6859  {
6860    curtop += obj.y;
6861  }
6862  return curtop;
6863};
6864
6865Xinha.createLoadingMessages = function(xinha_editors)
6866{
6867  if ( Xinha.loadingMessages || !Xinha.isSupportedBrowser )
6868  {
6869    return;
6870  }
6871  Xinha.loadingMessages = [];
6872 
6873  for (var i=0;i<xinha_editors.length;i++)
6874  {
6875     if (!document.getElementById(xinha_editors[i])) continue;
6876         Xinha.loadingMessages.push(Xinha.createLoadingMessage(Xinha.getElementById('textarea', xinha_editors[i])));
6877  }
6878};
6879
6880Xinha.createLoadingMessage = function(textarea,text)
6881{
6882  if ( document.getElementById("loading_" + textarea.id) || !Xinha.isSupportedBrowser)
6883  {
6884    return;
6885  }
6886  // Create and show the main loading message and the sub loading message for details of loading actions
6887  // global element
6888  var loading_message = document.createElement("div");
6889  loading_message.id = "loading_" + textarea.id;
6890  loading_message.className = "loading";
6891 
6892  loading_message.style.left = (Xinha.findPosX(textarea) + textarea.offsetWidth / 2) - 106 +  'px';
6893  loading_message.style.top = (Xinha.findPosY(textarea) + textarea.offsetHeight / 2) - 50 +  'px';
6894  // main static message
6895  var loading_main = document.createElement("div");
6896  loading_main.className = "loading_main";
6897  loading_main.id = "loading_main_" + textarea.id;
6898  loading_main.appendChild(document.createTextNode(Xinha._lc("Loading in progress. Please wait!")));
6899  // sub dynamic message
6900  var loading_sub = document.createElement("div");
6901  loading_sub.className = "loading_sub";
6902  loading_sub.id = "loading_sub_" + textarea.id;
6903  text = text ? text : Xinha._lc("Loading Core");
6904  loading_sub.appendChild(document.createTextNode(text));
6905  loading_message.appendChild(loading_main);
6906  loading_message.appendChild(loading_sub);
6907  document.body.appendChild(loading_message);
6908 
6909  Xinha.freeLater(loading_message);
6910  Xinha.freeLater(loading_main);
6911  Xinha.freeLater(loading_sub);
6912 
6913  return loading_sub;
6914};
6915
6916Xinha.prototype.setLoadingMessage = function(subMessage, mainMessage)
6917{
6918  if ( !document.getElementById("loading_sub_" + this._textArea.id) )
6919  {
6920    return;
6921  }
6922  document.getElementById("loading_main_" + this._textArea.id).innerHTML = mainMessage ? mainMessage : Xinha._lc("Loading in progress. Please wait!");
6923  document.getElementById("loading_sub_" + this._textArea.id).innerHTML = subMessage;
6924};
6925
6926Xinha.setLoadingMessage = function(string)
6927{
6928  if (!Xinha.loadingMessages) return; 
6929  for ( var i = 0; i < Xinha.loadingMessages.length; i++ )
6930  {
6931    Xinha.loadingMessages[i].innerHTML = string;
6932  }
6933};
6934
6935Xinha.prototype.removeLoadingMessage = function()
6936{
6937  if (document.getElementById("loading_" + this._textArea.id) )
6938  {
6939   document.body.removeChild(document.getElementById("loading_" + this._textArea.id));
6940  }
6941};
6942
6943Xinha.removeLoadingMessages = function(xinha_editors)
6944{
6945  for (var i=0;i< xinha_editors.length;i++)
6946  {
6947     if (!document.getElementById(xinha_editors[i])) continue;
6948     var main = document.getElementById("loading_" + document.getElementById(xinha_editors[i]).id);
6949     main.parentNode.removeChild(main);
6950  }
6951  Xinha.loadingMessages = null;
6952};
6953
6954/** List of objects that have to be trated on page unload in order to work around the broken
6955 * Garbage Collector in IE
6956 * @private
6957 * @see Xinha#freeLater
6958 * @see Xinha#free
6959 * @see Xinha#collectGarbageForIE
6960 */
6961Xinha.toFree = [];
6962/** Adds objects to Xinha.toFree
6963 * @param {Object} object The object to free memory
6964 * @param (String} prop optional  The property to release
6965 * @private
6966 * @see Xinha#toFree
6967 * @see Xinha#free
6968 * @see Xinha#collectGarbageForIE
6969 */
6970Xinha.freeLater = function(obj,prop)
6971{
6972  Xinha.toFree.push({o:obj,p:prop});
6973};
6974
6975/** Release memory properties from object
6976 * @param {Object} object The object to free memory
6977 * @param (String} prop optional The property to release
6978 * @private
6979 * @see Xinha#collectGarbageForIE
6980 * @see Xinha#free
6981 */
6982Xinha.free = function(obj, prop)
6983{
6984  if ( obj && !prop )
6985  {
6986    for ( var p in obj )
6987    {
6988      Xinha.free(obj, p);
6989    }
6990  }
6991  else if ( obj )
6992  {
6993    if ( prop.indexOf('src') == -1 ) // if src (also lowsrc, and maybe dynsrc ) is set to null, a file named "null" is requested from the server (see #1001)
6994    {
6995      try { obj[prop] = null; } catch(x) {}
6996    }
6997  }
6998};
6999
7000/** IE's Garbage Collector is broken very badly.  We will do our best to
7001 *   do it's job for it, but we can't be perfect. Takes all objects from Xinha.free and releases sets the null
7002 * @private
7003 * @see Xinha#toFree
7004 * @see Xinha#free
7005 */
7006
7007Xinha.collectGarbageForIE = function()
7008
7009  Xinha.flushEvents();   
7010  for ( var x = 0; x < Xinha.toFree.length; x++ )
7011  {
7012    Xinha.free(Xinha.toFree[x].o, Xinha.toFree[x].p);
7013    Xinha.toFree[x].o = null;
7014  }
7015};
7016
7017
7018// The following methods may be over-ridden or extended by the browser specific
7019// javascript files.
7020
7021
7022/** Insert a node at the current selection point.
7023 * @param {DomNode} toBeInserted
7024 */
7025
7026Xinha.prototype.insertNodeAtSelection = function(toBeInserted) { Xinha.notImplemented("insertNodeAtSelection"); }
7027
7028/** Get the parent element of the supplied or current selection.
7029 *  @param {Selection} sel optional selection as returned by getSelection
7030 *  @returns {DomNode}
7031 */
7032 
7033Xinha.prototype.getParentElement      = function(sel) { Xinha.notImplemented("getParentElement"); }
7034
7035/**
7036 * Returns the selected element, if any.  That is,
7037 * the element that you have last selected in the "path"
7038 * at the bottom of the editor, or a "control" (eg image)
7039 *
7040 * @returns {DomNode|null}
7041 */
7042 
7043Xinha.prototype.activeElement         = function(sel) { Xinha.notImplemented("activeElement"); }
7044
7045/**
7046 * Determines if the given selection is empty (collapsed).
7047 * @param {Selection} sel Selection object as returned by getSelection
7048 * @returns {Boolean}
7049 */
7050 
7051Xinha.prototype.selectionEmpty        = function(sel) { Xinha.notImplemented("selectionEmpty"); }
7052/**
7053 * Returns a range object to be stored
7054 * and later restored with Xinha.prototype.restoreSelection()
7055 * @returns {Range}
7056 */
7057
7058Xinha.prototype.saveSelection = function() { Xinha.notImplemented("saveSelection"); }
7059
7060/** Restores a selection previously stored
7061 * @param {Range} savedSelection Range object as returned by Xinha.prototype.restoreSelection()
7062 */
7063Xinha.prototype.restoreSelection = function(savedSelection)  { Xinha.notImplemented("restoreSelection"); }
7064
7065/**
7066 * Selects the contents of the given node.  If the node is a "control" type element, (image, form input, table)
7067 * the node itself is selected for manipulation.
7068 *
7069 * @param {DomNode} node
7070 * @param {Integer} pos  Set to a numeric position inside the node to collapse the cursor here if possible.
7071 */
7072Xinha.prototype.selectNodeContents    = function(node,pos) { Xinha.notImplemented("selectNodeContents"); }
7073
7074/** Insert HTML at the current position, deleting the selection if any.
7075 * 
7076 *  @param {String} html
7077 */
7078 
7079Xinha.prototype.insertHTML            = function(html) { Xinha.notImplemented("insertHTML"); }
7080
7081/** Get the HTML of the current selection.  HTML returned has not been passed through outwardHTML.
7082 *
7083 * @returns {String}
7084 */
7085Xinha.prototype.getSelectedHTML       = function() { Xinha.notImplemented("getSelectedHTML"); }
7086
7087/** Get a Selection object of the current selection.  Note that selection objects are browser specific.
7088 *
7089 * @returns {Selection}
7090 */
7091 
7092Xinha.prototype.getSelection          = function() { Xinha.notImplemented("getSelection"); }
7093
7094/** Create a Range object from the given selection.  Note that range objects are browser specific.
7095 *  @see Xinha#getSelection
7096 *  @param {Selection} sel Selection object
7097 *  @returns {Range}
7098 */
7099Xinha.prototype.createRange           = function(sel) { Xinha.notImplemented("createRange"); }
7100
7101/** Determine if the given event object is a keydown/press event.
7102 *
7103 *  @param {Event} event
7104 *  @returns {Boolean}
7105 */
7106 
7107Xinha.prototype.isKeyEvent            = function(event) { Xinha.notImplemented("isKeyEvent"); }
7108
7109/** Determines if the given key event object represents a combination of CTRL-<key>,
7110 *  which for Xinha is a shortcut.  Note that CTRL-ALT-<key> is not a shortcut.
7111 *
7112 *  @param    {Event} keyEvent
7113 *  @returns  {Boolean}
7114 */
7115 
7116Xinha.prototype.isShortCut = function(keyEvent)
7117{
7118  if(keyEvent.ctrlKey && !keyEvent.altKey)
7119  {
7120    return true;
7121  }
7122 
7123  return false;
7124}
7125
7126/** Return the character (as a string) of a keyEvent  - ie, press the 'a' key and
7127 *  this method will return 'a', press SHIFT-a and it will return 'A'.
7128 *
7129 *  @param   {Event} keyEvent
7130 *  @returns {String}
7131 */
7132                                   
7133Xinha.prototype.getKey = function(keyEvent) { Xinha.notImplemented("getKey"); }
7134
7135/** Return the HTML string of the given Element, including the Element.
7136 *
7137 * @param {DomNode} element HTML Element
7138 * @returns {String}
7139 */
7140 
7141Xinha.getOuterHTML = function(element) { Xinha.notImplemented("getOuterHTML"); }
7142
7143/** Get a new XMLHTTPRequest Object ready to be used.
7144 *
7145 * @returns {XMLHTTPRequest}
7146 */
7147
7148Xinha.getXMLHTTPRequestObject = function()
7149{
7150  try
7151  {   
7152    if (typeof XMLHttpRequest != "undefined" && typeof XMLHttpRequest.constructor == 'function' ) // Safari's XMLHttpRequest is typeof object
7153    {
7154          return new XMLHttpRequest();
7155    }
7156        else if (typeof ActiveXObject == "function")
7157        {
7158          return new ActiveXObject("Microsoft.XMLHTTP");
7159        }
7160  }
7161  catch(e)
7162  {
7163    Xinha.notImplemented('getXMLHTTPRequestObject');
7164  }
7165}
7166 
7167// Compatability - all these names are deprecated and will be removed in a future version
7168/** Alias of activeElement()
7169 * @see Xinha#activeElement
7170 * @deprecated
7171 * @returns {DomNode|null}
7172 */
7173Xinha.prototype._activeElement  = function(sel) { return this.activeElement(sel); }
7174/** Alias of selectionEmpty()
7175 * @see Xinha#selectionEmpty
7176 * @deprecated
7177 * @param {Selection} sel Selection object as returned by getSelection
7178 * @returns {Boolean}
7179 */
7180Xinha.prototype._selectionEmpty = function(sel) { return this.selectionEmpty(sel); }
7181/** Alias of getSelection()
7182 * @see Xinha#getSelection
7183 * @deprecated
7184 * @returns {Selection}
7185 */
7186Xinha.prototype._getSelection   = function() { return this.getSelection(); }
7187/** Alias of createRange()
7188 * @see Xinha#createRange
7189 * @deprecated
7190 * @param {Selection} sel Selection object
7191 * @returns {Range}
7192 */
7193Xinha.prototype._createRange    = function(sel) { return this.createRange(sel); }
7194HTMLArea = Xinha;
7195
7196Xinha.init();
7197
7198if ( Xinha.ie_version < 8 )
7199{
7200  Xinha.addDom0Event(window,'unload',Xinha.collectGarbageForIE);
7201}
7202Xinha.notImplemented = function(methodName)
7203{
7204  throw new Error("Method Not Implemented", "Part of Xinha has tried to call the " + methodName + " method which has not been implemented.");
7205}
Note: See TracBrowser for help on using the repository browser.