source: trunk/XinhaCore.js @ 998

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