source: trunk/modules/InternetExplorer/InternetExplorer.js

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

Rebuild the translation system and language files.

Includes a few kludges to help lc_parse_strings.php detect translation strings.

See README_TRANSLATORS.TXT for description of the translation system.

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