source: trunk/XinhaCore.js @ 1326

Last change on this file since 1326 was 1326, checked in by gogo, 6 years ago

#1594 - IE7 broken due to not having hasAttribute, implements a workalike

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