root / trunk / modules / InternetExplorer / InternetExplorer.js

Revision 1260, 29.3 kB (checked in by gogo, 4 months ago)

#1471 and #1508 Better fix for IE8 TableOperations (and others) and editing inside absolute positioned elements.

  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
Line 
1
2  /*--------------------------------------:noTabs=true:tabSize=2:indentSize=2:--
3    --  Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
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    --  Xinha was originally based on work by Mihai Bazon which is:
9    --      Copyright (c) 2003-2004 dynarch.com.
10    --      Copyright (c) 2002-2003 interactivetools.com, inc.
11    --      This copyright notice MUST stay intact for use.
12    --
13    -- This is the Internet Explorer compatability plugin, part of the
14    -- Xinha core.
15    --
16    --  The file is loaded as a special plugin by the Xinha Core when
17    --  Xinha is being run under an Internet Explorer based browser.
18    --
19    --  It provides implementation and specialisation for various methods
20    --  in the core where different approaches per browser are required.
21    --
22    --  Design Notes::
23    --   Most methods here will simply be overriding Xinha.prototype.<method>
24    --   and should be called that, but methods specific to IE should
25    --   be a part of the InternetExplorer.prototype, we won't trample on
26    --   namespace that way.
27    --
28    --  $HeadURL$
29    --  $LastChangedDate$
30    --  $LastChangedRevision$
31    --  $LastChangedBy$
32    --------------------------------------------------------------------------*/
33                                                   
34InternetExplorer._pluginInfo = {
35  name          : "Internet Explorer",
36  origin        : "Xinha Core",
37  version       : "$LastChangedRevision$".replace(/^[^:]*:\s*(.*)\s*\$$/, '$1'),
38  developer     : "The Xinha Core Developer Team",
39  developer_url : "$HeadURL$".replace(/^[^:]*:\s*(.*)\s*\$$/, '$1'),
40  sponsor       : "",
41  sponsor_url   : "",
42  license       : "htmlArea"
43};
44
45function InternetExplorer(editor) {
46  this.editor = editor; 
47  editor.InternetExplorer = this; // So we can do my_editor.InternetExplorer.doSomethingIESpecific();
48}
49
50/** Allow Internet Explorer to handle some key events in a special way.
51 */
52 
53InternetExplorer.prototype.onKeyPress = function(ev)
54{
55  // Shortcuts
56  if(this.editor.isShortCut(ev))
57  {
58    switch(this.editor.getKey(ev).toLowerCase())
59    {
60      case 'n':
61      {
62        this.editor.execCommand('formatblock', false, '<p>');       
63        Xinha._stopEvent(ev);
64        return true;
65      }
66      break;
67     
68      case '1':
69      case '2':
70      case '3':
71      case '4':
72      case '5':
73      case '6':
74      {
75        this.editor.execCommand('formatblock', false, '<h'+this.editor.getKey(ev).toLowerCase()+'>');
76        Xinha._stopEvent(ev);
77        return true;
78      }
79      break;
80    }
81  }
82 
83  switch(ev.keyCode)
84  {
85    case 8: // KEY backspace
86    case 46: // KEY delete
87    {
88      if(this.handleBackspace())
89      {
90        Xinha._stopEvent(ev);
91        return true;
92      }
93    }
94    break;
95   
96    case 9: // KEY tab, see ticket #1121
97    {
98      Xinha._stopEvent(ev);
99      return true;
100    }
101
102  }
103 
104  return false;
105}
106
107/** When backspace is hit, the IE onKeyPress will execute this method.
108 *  It preserves links when you backspace over them and apparently
109 *  deletes control elements (tables, images, form fields) in a better
110 *  way.
111 *
112 *  @returns true|false True when backspace has been handled specially
113 *   false otherwise (should pass through).
114 */
115
116InternetExplorer.prototype.handleBackspace = function()
117{
118  var editor = this.editor;
119  var sel = editor.getSelection();
120  if ( sel.type == 'Control' )
121  {
122    var elm = editor.activeElement(sel);
123    Xinha.removeFromParent(elm);
124    return true;
125  }
126
127  // This bit of code preseves links when you backspace over the
128  // endpoint of the link in IE.  Without it, if you have something like
129  //    link_here |
130  // where | is the cursor, and backspace over the last e, then the link
131  // will de-link, which is a bit tedious
132  var range = editor.createRange(sel);
133  var r2 = range.duplicate();
134  r2.moveStart("character", -1);
135  var a = r2.parentElement();
136  // @fixme: why using again a regex to test a single string ???
137  if ( a != range.parentElement() && ( /^a$/i.test(a.tagName) ) )
138  {
139    r2.collapse(true);
140    r2.moveEnd("character", 1);
141    r2.pasteHTML('');
142    r2.select();
143    return true;
144  }
145};
146
147InternetExplorer.prototype.inwardHtml = function(html)
148{
149   // Both IE and Gecko use strike internally instead of del (#523)
150   // Xinha will present del externally (see Xinha.prototype.outwardHtml
151   html = html.replace(/<(\/?)del(\s|>|\/)/ig, "<$1strike$2");
152   // ie eats scripts and comments at beginning of page, so
153   // make sure there is something before the first script on the page
154   html = html.replace(/(<script|<!--)/i,"&nbsp;$1");
155   
156   // We've got a workaround for certain issues with saving and restoring
157   // selections that may cause us to fill in junk span tags.  We'll clean
158   // those here
159   html = html.replace(/<span[^>]+id="__InsertSpan_Workaround_[a-z]+".*?>([\s\S]*?)<\/span>/i,"$1");
160   
161   return html;
162}
163
164InternetExplorer.prototype.outwardHtml = function(html)
165{
166   // remove space added before first script on the page
167   html = html.replace(/&nbsp;(\s*)(<script|<!--)/i,"$1$2");
168
169   // We've got a workaround for certain issues with saving and restoring
170   // selections that may cause us to fill in junk span tags.  We'll clean
171   // those here
172   html = html.replace(/<span[^>]+id="__InsertSpan_Workaround_[a-z]+".*?>([\s\S]*?)<\/span>/i,"$1");
173   
174   return html;
175}
176
177InternetExplorer.prototype.onExecCommand = function(cmdID, UI, param)
178{   
179  switch(cmdID)
180  {
181    // #645 IE only saves the initial content of the iframe, so we create a temporary iframe with the current editor contents
182    case 'saveas':
183        var doc = null;
184        var editor = this.editor;
185        var iframe = document.createElement("iframe");
186        iframe.src = "about:blank";
187        iframe.style.display = 'none';
188        document.body.appendChild(iframe);
189        try
190        {
191          if ( iframe.contentDocument )
192          {
193            doc = iframe.contentDocument;       
194          }
195          else
196          {
197            doc = iframe.contentWindow.document;
198          }
199        }
200        catch(ex)
201        {
202          //hope there's no exception
203        }
204       
205        doc.open("text/html","replace");
206        var html = '';
207        if ( editor.config.browserQuirksMode === false )
208        {
209          var doctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
210        }
211        else if ( editor.config.browserQuirksMode === true )
212        {
213           var doctype = '';
214        }
215        else
216        {
217           var doctype = Xinha.getDoctype(document);
218        }
219        if ( !editor.config.fullPage )
220        {
221          html += doctype + "\n";
222          html += "<html>\n";
223          html += "<head>\n";
224          html += "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + editor.config.charSet + "\">\n";
225          if ( typeof editor.config.baseHref != 'undefined' && editor.config.baseHref !== null )
226          {
227            html += "<base href=\"" + editor.config.baseHref + "\"/>\n";
228          }
229         
230          if ( typeof editor.config.pageStyleSheets !== 'undefined' )
231          {
232            for ( var i = 0; i < editor.config.pageStyleSheets.length; i++ )
233            {
234              if ( editor.config.pageStyleSheets[i].length > 0 )
235              {
236                html += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + editor.config.pageStyleSheets[i] + "\">";
237                //html += "<style> @import url('" + editor.config.pageStyleSheets[i] + "'); </style>\n";
238              }
239            }
240          }
241         
242          if ( editor.config.pageStyle )
243          {
244            html += "<style type=\"text/css\">\n" + editor.config.pageStyle + "\n</style>";
245          }
246         
247          html += "</head>\n";
248          html += "<body>\n";
249          html += editor.getEditorContent();
250          html += "</body>\n";
251          html += "</html>";
252        }
253        else
254        {
255          html = editor.getEditorContent();
256          if ( html.match(Xinha.RE_doctype) )
257          {
258            editor.setDoctype(RegExp.$1);
259          }
260        }
261        doc.write(html);
262        doc.close();
263        doc.execCommand(cmdID, UI, param);
264        document.body.removeChild(iframe);
265      return true;
266    break;
267    case 'removeformat':
268      var editor = this.editor;
269      var sel = editor.getSelection();
270      var selSave = editor.saveSelection(sel);
271
272      var i, el, els;
273
274      function clean (el)
275      {
276        if (el.nodeType != 1) return;
277        el.removeAttribute('style');
278        for (var j=0; j<el.childNodes.length;j++)
279        {
280          clean(el.childNodes[j]);
281        }
282        if ( (el.tagName.toLowerCase() == 'span' && !el.attributes.length ) || el.tagName.toLowerCase() == 'font')
283        {
284          el.outerHTML = el.innerHTML;
285        }
286      }
287      if ( editor.selectionEmpty(sel) )
288      {
289        els = editor._doc.body.childNodes;
290        for (i = 0; i < els.length; i++)
291        {
292          el = els[i];
293          if (el.nodeType != 1) continue;
294          if (el.tagName.toLowerCase() == 'span')
295          {
296            newNode = editor.convertNode(el, 'div');
297            el.parentNode.replaceChild(newNode, el);
298            el = newNode;
299          }
300          clean(el);
301        }
302      }
303      editor._doc.execCommand(cmdID, UI, param);
304
305      editor.restoreSelection(selSave);
306      return true;
307    break;
308  }
309 
310  return false;
311};
312/*--------------------------------------------------------------------------*/
313/*------- IMPLEMENTATION OF THE ABSTRACT "Xinha.prototype" METHODS ---------*/
314/*--------------------------------------------------------------------------*/
315
316/** Insert a node at the current selection point.
317 * @param toBeInserted DomNode
318 */
319
320Xinha.prototype.insertNodeAtSelection = function(toBeInserted)
321{
322  this.insertHTML(toBeInserted.outerHTML);
323};
324
325 
326/** Get the parent element of the supplied or current selection.
327 *  @param   sel optional selection as returned by getSelection
328 *  @returns DomNode
329 */
330 
331Xinha.prototype.getParentElement = function(sel)
332{
333  if ( typeof sel == 'undefined' )
334  {
335    sel = this.getSelection();
336  }
337  var range = this.createRange(sel);
338  switch ( sel.type )
339  {
340    case "Text":
341      // try to circumvent a bug in IE:
342      // the parent returned is not always the real parent element
343      var parent = range.parentElement();
344      while ( true )
345      {
346        var TestRange = range.duplicate();
347        TestRange.moveToElementText(parent);
348        if ( TestRange.inRange(range) )
349        {
350          break;
351        }
352        if ( ( parent.nodeType != 1 ) || ( parent.tagName.toLowerCase() == 'body' ) )
353        {
354          break;
355        }
356        parent = parent.parentElement;
357      }
358      return parent;
359    case "None":
360      // It seems that even for selection of type "None",
361      // there _is_ a parent element and it's value is not
362      // only correct, but very important to us.  MSIE is
363      // certainly the buggiest browser in the world and I
364      // wonder, God, how can Earth stand it?
365      try
366      {
367        return range.parentElement();
368      }
369      catch(e)
370      {
371        return this._doc.body; // ??
372      }
373     
374    case "Control":
375      return range.item(0);
376    default:
377      return this._doc.body;
378  }
379};
380 
381/**
382 * Returns the selected element, if any.  That is,
383 * the element that you have last selected in the "path"
384 * at the bottom of the editor, or a "control" (eg image)
385 *
386 * @returns null | DomNode
387 */
388 
389Xinha.prototype.activeElement = function(sel)
390{
391  if ( ( sel === null ) || this.selectionEmpty(sel) )
392  {
393    return null;
394  }
395
396  if ( sel.type.toLowerCase() == "control" )
397  {
398    return sel.createRange().item(0);
399  }
400  else
401  {
402    // If it's not a control, then we need to see if
403    // the selection is the _entire_ text of a parent node
404    // (this happens when a node is clicked in the tree)
405    var range = sel.createRange();
406    var p_elm = this.getParentElement(sel);
407    if ( p_elm.innerHTML == range.htmlText )
408    {
409      return p_elm;
410    }
411    /*
412    if ( p_elm )
413    {
414      var p_rng = this._doc.body.createTextRange();
415      p_rng.moveToElementText(p_elm);
416      if ( p_rng.isEqual(range) )
417      {
418        return p_elm;
419      }
420    }
421
422    if ( range.parentElement() )
423    {
424      var prnt_range = this._doc.body.createTextRange();
425      prnt_range.moveToElementText(range.parentElement());
426      if ( prnt_range.isEqual(range) )
427      {
428        return range.parentElement();
429      }
430    }
431    */
432    return null;
433  }
434};
435
436/**
437 * Determines if the given selection is empty (collapsed).
438 * @param selection Selection object as returned by getSelection
439 * @returns true|false
440 */
441 
442Xinha.prototype.selectionEmpty = function(sel)
443{
444  if ( !sel )
445  {
446    return true;
447  }
448
449  return this.createRange(sel).htmlText === '';
450};
451
452/**
453 * Returns a range object to be stored
454 * and later restored with Xinha.prototype.restoreSelection()
455 *
456 * @returns Range
457 */
458Xinha.prototype.saveSelection = function(sel)
459{
460  return this.createRange(sel ? sel : this.getSelection())
461}
462/**
463 * Restores a selection previously stored
464 * @param savedSelection Range object as returned by Xinha.prototype.restoreSelection()
465 */
466Xinha.prototype.restoreSelection = function(savedSelection)
467{
468  if (!savedSelection) return;
469 
470  // Ticket #1387
471  // avoid problem where savedSelection does not implement parentElement().
472  // This condition occurs if there was no text selection at the time saveSelection() was called.  In the case
473  // an image selection, the situation is confusing... the image may be selected in two different ways:  1) by
474  // simply clicking the image it will appear to be selected by a box with sizing handles; or 2) by clicking and
475  // dragging over the image as you might click and drag over text.  In the first case, the resulting selection
476  // object does not implement parentElement(), leading to a crash later on in the code below.  The following
477  // hack avoids that problem.
478 
479  // Ticket #1488
480  // fix control selection in IE8
481 
482  var savedParentElement = null;
483  if (savedSelection.parentElement)
484  {
485    savedParentElement =  savedSelection.parentElement();
486  }
487  else
488  {
489    savedParentElement = savedSelection.item(0);
490  }
491 
492  // In order to prevent triggering the IE bug mentioned below, we will try to
493  // optimize by not restoring the selection if it happens to match the current
494  // selection.
495  var range = this.createRange(this.getSelection());
496
497  var rangeParentElement =  null;
498  if (range.parentElement)
499  {
500    rangeParentElement =  range.parentElement();
501  }
502  else
503  {
504    rangeParentElement = range.item(0);
505  }
506
507  // We can't compare two selections that come from different documents, so we
508  // must make sure they're from the same document.
509  var findDoc = function(el)
510  {
511    for (var root=el; root; root=root.parentNode)
512    {
513      if (root.tagName.toLowerCase() == 'html')
514      {
515        return root.parentNode;
516      }
517    }
518    return null;
519  }
520
521  if (savedSelection.parentElement && findDoc(savedParentElement) == findDoc(rangeParentElement))
522  {
523    if (range.isEqual(savedSelection))
524    {
525      // The selection hasn't moved, no need to restore.
526      return;
527    }
528  }
529
530  try { savedSelection.select() } catch (e) {};
531  range = this.createRange(this.getSelection());
532 
533  if (range.parentElement)
534  {
535    rangeParentElement =  range.parentElement();
536  }
537  else
538  {
539    rangeParentElement = range.item(0);
540  }
541 
542  if (rangeParentElement != savedParentElement)
543  {
544    // IE has a problem with selections at the end of text nodes that
545    // immediately precede block nodes. Example markup:
546    // <div>Text Node<p>Text in Block</p></div>
547    //               ^
548    // The problem occurs when the cursor is after the 'e' in Node.
549
550    var solution = this.config.selectWorkaround || 'VisibleCue';
551    switch (solution)
552    {
553      case 'SimulateClick':
554        // Try to get the bounding box of the selection and then simulate a
555        // mouse click in the upper right corner to return the cursor to the
556        // correct location.
557
558        // No code yet, fall through to InsertSpan
559      case 'InsertSpan':
560        // This workaround inserts an empty span element so that we are no
561        // longer trying to select a text node,
562        var parentDoc = findDoc(savedParentElement);
563
564        // A function used to generate a unique ID for our temporary span.
565        var randLetters = function(count)
566        {
567          // Build a list of 26 letters.
568          var Letters = '';
569          for (var index = 0; index<26; ++index)
570          {
571            Letters += String.fromCharCode('a'.charCodeAt(0) + index);
572          }
573
574          var result = '';
575          for (var index=0; index<count; ++index)
576          {
577            result += Letters.substr(Math.floor(Math.random()*Letters.length + 1), 1);
578          }
579          return result;
580        }
581
582        // We'll try to find a unique ID to use for finding our element.
583        var keyLength = 1;
584        var tempId = '__InsertSpan_Workaround_' + randLetters(keyLength);
585        while (parentDoc.getElementById(tempId))
586        {
587          // Each time there's a collision, we'll increase our key length by
588          // one, making the chances of a collision exponentially more rare.
589          keyLength += 1;
590          tempId = '__InsertSpan_Workaround_' + randLetters(keyLength);
591        }
592
593        // Now that we have a uniquely identifiable element, we'll stick it and
594        // and use it to orient our selection.
595        savedSelection.pasteHTML('<span id="' + tempId + '"></span>');
596        var tempSpan = parentDoc.getElementById(tempId);
597        savedSelection.moveToElementText(tempSpan);
598        savedSelection.select();
599        break;
600      case 'JustificationHack':
601        // Setting the justification on an element causes IE to alter the
602        // markup so that the selection we want to make is possible.
603        // Unfortunately, this can force block elements to be kicked out of
604        // their containing element, so it is not recommended.
605
606        // Set a non-valid character and use it to anchor our selection.
607        var magicString = String.fromCharCode(1);
608        savedSelection.pasteHTML(magicString);
609        savedSelection.findText(magicString,-1);
610        savedSelection.select();
611
612        // I don't know how to find out if there's an existing justification on
613        // this element.  Hopefully, you're doing all of your styling outside,
614        // so I'll just clear.  I already told you this was a hack.
615        savedSelection.execCommand('JustifyNone');
616        savedSelection.pasteHTML('');
617        break;
618      case 'VisibleCue':
619      default:
620        // This method will insert a little box character to hold our selection
621        // in the desired spot.  We're depending on the user to see this ugly
622        // box and delete it themselves.
623        var magicString = String.fromCharCode(1);
624        savedSelection.pasteHTML(magicString);
625        savedSelection.findText(magicString,-1);
626        savedSelection.select();
627    }
628  }
629}
630
631/**
632 * Selects the contents of the given node.  If the node is a "control" type element, (image, form input, table)
633 * the node itself is selected for manipulation.
634 *
635 * @param node DomNode
636 * @param collapseToStart A boolean that, when supplied, says to collapse the selection. True collapses to the start, and false to the end.
637 */
638 
639Xinha.prototype.selectNodeContents = function(node, collapseToStart)
640{
641  this.focusEditor();
642  this.forceRedraw();
643  var range;
644  var collapsed = typeof collapseToStart == "undefined" ? true : false;
645  // Tables and Images get selected as "objects" rather than the text contents
646  if ( collapsed && node.tagName && node.tagName.toLowerCase().match(/table|img|input|select|textarea/) )
647  {
648    range = this._doc.body.createControlRange();
649    range.add(node);
650  }
651  else
652  {
653    range = this._doc.body.createTextRange();
654    if (3 == node.nodeType)
655    {
656      // Special handling for text nodes, since moveToElementText fails when
657      // attempting to select a text node
658
659      // Since the TextRange has a quite limited API, our strategy here is to
660      // select (where possible) neighboring nodes, and then move our ranges
661      // endpoints to be just inside of neighboring selections.
662      if (node.parentNode)
663      {
664        range.moveToElementText(node.parentNode);
665      } else
666      {
667        range.moveToElementText(this._doc.body);
668      }
669      var trimmingRange = this._doc.body.createTextRange();
670
671      // In rare situations (mostly html that's been monkeyed about with by
672      // javascript, but that's what we're doing) there can be two adjacent
673      // text nodes.  Since we won't be able to handle these, we'll have to
674      // hack an offset by 'move'ing the number of characters they contain.
675      var texthackOffset = 0;
676      var borderElement=node.previousSibling;
677      for (; borderElement && (1 != borderElement.nodeType); borderElement = borderElement.previousSibling)
678      {
679        if (3 == borderElement.nodeType)
680        {
681          // IE doesn't count '\r' as a character, so we have to adjust the offset.
682          texthackOffset += borderElement.nodeValue.length-borderElement.nodeValue.split('\r').length-1;
683        }
684      }
685      if (borderElement && (1 == borderElement.nodeType))
686      {
687        trimmingRange.moveToElementText(borderElement);
688        range.setEndPoint('StartToEnd', trimmingRange);
689      }
690      if (texthackOffset)
691      {
692        // We now need to move the selection forward the number of characters
693        // in all text nodes in between our text node and our ranges starting
694        // border.
695        range.moveStart('character',texthackOffset);
696      }
697
698      // Youpi!  Now we get to repeat this trimming on the right side.
699      texthackOffset = 0;
700      borderElement=node.nextSibling;
701      for (; borderElement && (1 != borderElement.nodeType); borderElement = borderElement.nextSibling)
702      {
703        if (3 == borderElement.nodeType)
704        {
705          // IE doesn't count '\r' as a character, so we have to adjust the offset.
706          texthackOffset += borderElement.nodeValue.length-borderElement.nodeValue.split('\r').length-1;
707          if (!borderElement.nextSibling)
708          {
709            // When a text node is the last child, IE adds an extra selection
710            // "placeholder" for the newline character.  We need to adjust for
711            // this character as well.
712            texthackOffset += 1;
713          }
714        }
715      }
716      if (borderElement && (1 == borderElement.nodeType))
717      {
718        trimmingRange.moveToElementText(borderElement);
719        range.setEndPoint('EndToStart', trimmingRange);
720      }
721      if (texthackOffset)
722      {
723        // We now need to move the selection backward the number of characters
724        // in all text nodes in between our text node and our ranges ending
725        // border.
726        range.moveEnd('character',-texthackOffset);
727      }
728      if (!node.nextSibling)
729      {
730        // Above we performed a slight adjustment to the offset if the text
731        // node contains a selectable "newline".  We need to do the same if the
732        // node we are trying to select contains a newline.
733        range.moveEnd('character',-1);
734      }
735    }
736    else
737    {
738    range.moveToElementText(node);
739    }
740  }
741  if (typeof collapseToStart != "undefined")
742  {
743    range.collapse(collapseToStart);
744    if (!collapseToStart)
745    {
746      range.moveStart('character',-1);
747      range.moveEnd('character',-1);
748    }
749  }
750  range.select();
751};
752 
753/** Insert HTML at the current position, deleting the selection if any.
754 * 
755 *  @param html string
756 */
757 
758Xinha.prototype.insertHTML = function(html)
759{
760  this.focusEditor();
761  var sel = this.getSelection();
762  var range = this.createRange(sel);
763  range.pasteHTML(html);
764};
765
766
767/** Get the HTML of the current selection.  HTML returned has not been passed through outwardHTML.
768 *
769 * @returns string
770 */
771 
772Xinha.prototype.getSelectedHTML = function()
773{
774  var sel = this.getSelection();
775  if (this.selectionEmpty(sel)) return '';
776  var range = this.createRange(sel);
777 
778  // Need to be careful of control ranges which won't have htmlText
779  if( range.htmlText )
780  {
781    return range.htmlText;
782  }
783  else if(range.length >= 1)
784  {
785    return range.item(0).outerHTML;
786  }
787 
788  return '';
789};
790 
791/** Get a Selection object of the current selection.  Note that selection objects are browser specific.
792 *
793 * @returns Selection
794 */
795 
796Xinha.prototype.getSelection = function()
797{
798  return this._doc.selection;
799};
800
801/** Create a Range object from the given selection.  Note that range objects are browser specific.
802 *
803 *  @param sel Selection object (see getSelection)
804 *  @returns Range
805 */
806 
807Xinha.prototype.createRange = function(sel)
808{
809  if (!sel) sel = this.getSelection();
810 
811  // ticket:1508 - when you do a key event within a
812  // absolute position div, in IE, the toolbar update
813  // for formatblock etc causes a getParentElement() (above)
814  // which produces a "None" select, then if we focusEditor() it
815  // defocuses the absolute div and focuses into the iframe outside of the
816  // div somewhere. 
817  //
818  // Removing this is probably a workaround and maybe it breaks something else
819  // focusEditor is used in a number of spots, I woudl have thought it should
820  // do nothing if the editor is already focused.
821  //
822  // if(sel.type == 'None') this.focusEditor();
823 
824  return sel.createRange();
825};
826
827/** Determine if the given event object is a keydown/press event.
828 *
829 *  @param event Event
830 *  @returns true|false
831 */
832 
833Xinha.prototype.isKeyEvent = function(event)
834{
835  return event.type == "keydown";
836}
837
838/** Return the character (as a string) of a keyEvent  - ie, press the 'a' key and
839 *  this method will return 'a', press SHIFT-a and it will return 'A'.
840 *
841 *  @param   keyEvent
842 *  @returns string
843 */
844                                   
845Xinha.prototype.getKey = function(keyEvent)
846{
847  return String.fromCharCode(keyEvent.keyCode);
848}
849
850
851/** Return the HTML string of the given Element, including the Element.
852 *
853 * @param element HTML Element DomNode
854 * @returns string
855 */
856 
857Xinha.getOuterHTML = function(element)
858{
859  return element.outerHTML;
860};
861
862// Control character for retaining edit location when switching modes
863Xinha.cc = String.fromCharCode(0x2009);
864
865Xinha.prototype.setCC = function ( target )
866{
867  var cc = Xinha.cc;
868  if ( target == "textarea" )
869  {
870    var ta = this._textArea;
871    var pos = document.selection.createRange();
872    pos.collapse();
873    pos.text = cc;
874    var index = ta.value.indexOf( cc );
875    var before = ta.value.substring( 0, index );
876    var after  = ta.value.substring( index + cc.length , ta.value.length );
877   
878    if ( after.match(/^[^<]*>/) ) // make sure cursor is in an editable area (outside tags, script blocks, entities, and inside the body)
879    {
880      var tagEnd = after.indexOf(">") + 1;
881      ta.value = before + after.substring( 0, tagEnd ) + cc + after.substring( tagEnd, after.length );
882    }
883    else ta.value = before + cc + after;
884    ta.value = ta.value.replace(new RegExp ('(&[^'+cc+']*?)('+cc+')([^'+cc+']*?;)'), "$1$3$2");
885    ta.value = ta.value.replace(new RegExp ('(<script[^>]*>[^'+cc+']*?)('+cc+')([^'+cc+']*?<\/script>)'), "$1$3$2");
886    ta.value = ta.value.replace(new RegExp ('^([^'+cc+']*)('+cc+')([^'+cc+']*<body[^>]*>)(.*?)'), "$1$3$2$4");
887  }
888  else
889  {
890    var sel = this.getSelection();
891    var r = sel.createRange();
892    if ( sel.type == 'Control' )
893    {
894      var control = r.item(0);
895      control.outerHTML += cc;
896    }
897    else
898    {
899      r.collapse();
900      r.text = cc;
901    }
902  }
903};
904
905Xinha.prototype.findCC = function ( target )
906{
907  var findIn = ( target == 'textarea' ) ? this._textArea : this._doc.body;
908  range = findIn.createTextRange();
909  // in case the cursor is inside a link automatically created from a url
910  // the cc also appears in the url and we have to strip it out additionally
911  if( range.findText( escape(Xinha.cc) ) )
912  {
913    range.select();
914    range.text = '';
915    range.select();
916  }
917  if( range.findText( Xinha.cc ) )
918  {
919    range.select();
920    range.text = '';
921    range.select();
922  }
923  if ( target == 'textarea' ) this._textArea.focus();
924};
925
926/** Return a doctype or empty string depending on whether the document is in Qirksmode or Standards Compliant Mode
927 *  It's hardly possible to detect the actual doctype without unreasonable effort, so we set HTML 4.01 just to trigger the rendering mode
928 *
929 * @param doc DOM element document
930 * @returns string doctype || empty
931 */
932Xinha.getDoctype = function (doc)
933{
934  return (doc.compatMode == "CSS1Compat" && Xinha.ie_version < 8 ) ? '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">' : '';
935};
Note: See TracBrowser for help on using the browser.