root / branches / new-dialogs / XinhaCore.js

Revision 1026, 211.9 kB (checked in by ray, 3 years ago)

#1176 added global config options for centering, greyout, and close on escape

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