source: trunk/XinhaCore.js @ 1332

Last change on this file since 1332 was 1332, checked in by gogo, 8 months ago

Pushing some old updates up from my current production system...

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