source: trunk/modules/InternetExplorer/InternetExplorer.js @ 999

Last change on this file since 999 was 999, checked in by ray, 11 years ago

#1195 Allow to specify an external url to load a plugin from

  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
File size: 18.7 KB
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:http://svn.xinha.webfactional.com/trunk/modules/InternetExplorer/InternetExplorer.js $
29    --  $LastChangedDate:2008-03-21 18:16:28 +0100 (Fr, 21 Mrz 2008) $
30    --  $LastChangedRevision:980 $
31    --  $LastChangedBy:ray $
32    --------------------------------------------------------------------------*/
33                                                   
34InternetExplorer._pluginInfo = {
35  name          : "Internet Explorer",
36  origin        : "Xinha Core",
37  version       : "$LastChangedRevision:980 $".replace(/^[^:]*: (.*) \$$/, '$1'),
38  developer     : "The Xinha Core Developer Team",
39  developer_url : "$HeadURL:http://svn.xinha.webfactional.com/trunk/modules/InternetExplorer/InternetExplorer.js $".replace(/^[^:]*: (.*) \$$/, '$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 
97  return false;
98}
99
100/** When backspace is hit, the IE onKeyPress will execute this method.
101 *  It preserves links when you backspace over them and apparently
102 *  deletes control elements (tables, images, form fields) in a better
103 *  way.
104 *
105 *  @returns true|false True when backspace has been handled specially
106 *   false otherwise (should pass through).
107 */
108
109InternetExplorer.prototype.handleBackspace = function()
110{
111  var editor = this.editor;
112  var sel = editor.getSelection();
113  if ( sel.type == 'Control' )
114  {
115    var elm = editor.activeElement(sel);
116    Xinha.removeFromParent(elm);
117    return true;
118  }
119
120  // This bit of code preseves links when you backspace over the
121  // endpoint of the link in IE.  Without it, if you have something like
122  //    link_here |
123  // where | is the cursor, and backspace over the last e, then the link
124  // will de-link, which is a bit tedious
125  var range = editor.createRange(sel);
126  var r2 = range.duplicate();
127  r2.moveStart("character", -1);
128  var a = r2.parentElement();
129  // @fixme: why using again a regex to test a single string ???
130  if ( a != range.parentElement() && ( /^a$/i.test(a.tagName) ) )
131  {
132    r2.collapse(true);
133    r2.moveEnd("character", 1);
134    r2.pasteHTML('');
135    r2.select();
136    return true;
137  }
138};
139
140InternetExplorer.prototype.inwardHtml = function(html)
141{
142   // Both IE and Gecko use strike internally instead of del (#523)
143   // Xinha will present del externally (see Xinha.prototype.outwardHtml
144   html = html.replace(/<(\/?)del(\s|>|\/)/ig, "<$1strike$2");
145   // ie eats scripts and comments at beginning of page, so
146   // make sure there is something before the first script on the page
147   html = html.replace(/(<script|<!--)/i,"&nbsp;$1");
148   
149   return html;
150}
151
152InternetExplorer.prototype.outwardHtml = function(html)
153{
154   // remove space added before first script on the page
155   html = html.replace(/&nbsp;(\s*)(<script|<!--)/i,"$1$2");
156   
157   return html;
158}
159
160InternetExplorer.prototype.onExecCommand = function(cmdID, UI, param)
161{   
162  switch(cmdID)
163  {
164    // #645 IE only saves the initial content of the iframe, so we create a temporary iframe with the current editor contents
165    case 'saveas':
166        var doc = null;
167        var editor = this.editor;
168        var iframe = document.createElement("iframe");
169        iframe.src = "about:blank";
170        iframe.style.display = 'none';
171        document.body.appendChild(iframe);
172        try
173        {
174          if ( iframe.contentDocument )
175          {
176            doc = iframe.contentDocument;       
177          }
178          else
179          {
180            doc = iframe.contentWindow.document;
181          }
182        }
183        catch(ex)
184        {
185          //hope there's no exception
186        }
187       
188        doc.open("text/html","replace");
189        var html = '';
190        if ( editor.config.browserQuirksMode === false )
191        {
192          var doctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
193        }
194        else if ( editor.config.browserQuirksMode === true )
195        {
196           var doctype = '';
197        }
198        else
199        {
200           var doctype = Xinha.getDoctype(document);
201        }
202        if ( !editor.config.fullPage )
203        {
204          html += doctype + "\n";
205          html += "<html>\n";
206          html += "<head>\n";
207          html += "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + editor.config.charSet + "\">\n";
208          if ( typeof editor.config.baseHref != 'undefined' && editor.config.baseHref !== null )
209          {
210            html += "<base href=\"" + editor.config.baseHref + "\"/>\n";
211          }
212         
213          if ( typeof editor.config.pageStyleSheets !== 'undefined' )
214          {
215            for ( var i = 0; i < editor.config.pageStyleSheets.length; i++ )
216            {
217              if ( editor.config.pageStyleSheets[i].length > 0 )
218              {
219                html += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + editor.config.pageStyleSheets[i] + "\">";
220                //html += "<style> @import url('" + editor.config.pageStyleSheets[i] + "'); </style>\n";
221              }
222            }
223          }
224         
225          if ( editor.config.pageStyle )
226          {
227            html += "<style type=\"text/css\">\n" + editor.config.pageStyle + "\n</style>";
228          }
229         
230          html += "</head>\n";
231          html += "<body>\n";
232          html += editor.getEditorContent();
233          html += "</body>\n";
234          html += "</html>";
235        }
236        else
237        {
238          html = editor.getEditorContent();
239          if ( html.match(Xinha.RE_doctype) )
240          {
241            editor.setDoctype(RegExp.$1);
242          }
243        }
244        doc.write(html);
245        doc.close();
246        doc.execCommand(cmdID, UI, param);
247        document.body.removeChild(iframe);
248      return true;
249    break;
250    case 'removeformat':
251      var editor = this.editor;
252      var sel = editor.getSelection();
253      var selSave = editor.saveSelection(sel);
254
255      var i, el, els;
256
257      function clean (el)
258      {
259        if (el.nodeType != 1) return;
260        el.removeAttribute('style');
261        for (var j=0; j<el.childNodes.length;j++)
262        {
263          clean(el.childNodes[j]);
264        }
265        if ( (el.tagName.toLowerCase() == 'span' && !el.attributes.length ) || el.tagName.toLowerCase() == 'font')
266        {
267          el.outerHTML = el.innerHTML;
268        }
269      }
270      if ( editor.selectionEmpty(sel) )
271      {
272        els = editor._doc.body.childNodes;
273        for (i = 0; i < els.length; i++)
274        {
275          el = els[i];
276          if (el.nodeType != 1) continue;
277          if (el.tagName.toLowerCase() == 'span')
278          {
279            newNode = editor.convertNode(el, 'div');
280            el.parentNode.replaceChild(newNode, el);
281            el = newNode;
282          }
283          clean(el);
284        }
285      }
286      editor._doc.execCommand(cmdID, UI, param);
287
288      editor.restoreSelection(selSave);
289      return true;
290    break;
291  }
292 
293  return false;
294};
295/*--------------------------------------------------------------------------*/
296/*------- IMPLEMENTATION OF THE ABSTRACT "Xinha.prototype" METHODS ---------*/
297/*--------------------------------------------------------------------------*/
298
299/** Insert a node at the current selection point.
300 * @param toBeInserted DomNode
301 */
302
303Xinha.prototype.insertNodeAtSelection = function(toBeInserted)
304{
305  this.insertHTML(toBeInserted.outerHTML);
306};
307
308 
309/** Get the parent element of the supplied or current selection.
310 *  @param   sel optional selection as returned by getSelection
311 *  @returns DomNode
312 */
313 
314Xinha.prototype.getParentElement = function(sel)
315{
316  if ( typeof sel == 'undefined' )
317  {
318    sel = this.getSelection();
319  }
320  var range = this.createRange(sel);
321  switch ( sel.type )
322  {
323    case "Text":
324      // try to circumvent a bug in IE:
325      // the parent returned is not always the real parent element
326      var parent = range.parentElement();
327      while ( true )
328      {
329        var TestRange = range.duplicate();
330        TestRange.moveToElementText(parent);
331        if ( TestRange.inRange(range) )
332        {
333          break;
334        }
335        if ( ( parent.nodeType != 1 ) || ( parent.tagName.toLowerCase() == 'body' ) )
336        {
337          break;
338        }
339        parent = parent.parentElement;
340      }
341      return parent;
342    case "None":
343      // It seems that even for selection of type "None",
344      // there _is_ a parent element and it's value is not
345      // only correct, but very important to us.  MSIE is
346      // certainly the buggiest browser in the world and I
347      // wonder, God, how can Earth stand it?
348      return range.parentElement();
349    case "Control":
350      return range.item(0);
351    default:
352      return this._doc.body;
353  }
354};
355 
356/**
357 * Returns the selected element, if any.  That is,
358 * the element that you have last selected in the "path"
359 * at the bottom of the editor, or a "control" (eg image)
360 *
361 * @returns null | DomNode
362 */
363 
364Xinha.prototype.activeElement = function(sel)
365{
366  if ( ( sel === null ) || this.selectionEmpty(sel) )
367  {
368    return null;
369  }
370
371  if ( sel.type.toLowerCase() == "control" )
372  {
373    return sel.createRange().item(0);
374  }
375  else
376  {
377    // If it's not a control, then we need to see if
378    // the selection is the _entire_ text of a parent node
379    // (this happens when a node is clicked in the tree)
380    var range = sel.createRange();
381    var p_elm = this.getParentElement(sel);
382    if ( p_elm.innerHTML == range.htmlText )
383    {
384      return p_elm;
385    }
386    /*
387    if ( p_elm )
388    {
389      var p_rng = this._doc.body.createTextRange();
390      p_rng.moveToElementText(p_elm);
391      if ( p_rng.isEqual(range) )
392      {
393        return p_elm;
394      }
395    }
396
397    if ( range.parentElement() )
398    {
399      var prnt_range = this._doc.body.createTextRange();
400      prnt_range.moveToElementText(range.parentElement());
401      if ( prnt_range.isEqual(range) )
402      {
403        return range.parentElement();
404      }
405    }
406    */
407    return null;
408  }
409};
410
411/**
412 * Determines if the given selection is empty (collapsed).
413 * @param selection Selection object as returned by getSelection
414 * @returns true|false
415 */
416 
417Xinha.prototype.selectionEmpty = function(sel)
418{
419  if ( !sel )
420  {
421    return true;
422  }
423
424  return this.createRange(sel).htmlText === '';
425};
426
427/**
428 * Returns a range object to be stored
429 * and later restored with Xinha.prototype.restoreSelection()
430 *
431 * @returns Range
432 */
433Xinha.prototype.saveSelection = function()
434{
435  return this.createRange(this.getSelection())
436}
437/**
438 * Restores a selection previously stored
439 * @param savedSelection Range object as returned by Xinha.prototype.restoreSelection()
440 */
441Xinha.prototype.restoreSelection = function(savedSelection)
442{
443  try { savedSelection.select() } catch (e) {};
444}
445
446/**
447 * Selects the contents of the given node.  If the node is a "control" type element, (image, form input, table)
448 * the node itself is selected for manipulation.
449 *
450 * @param node DomNode
451 * @param pos  Set to a numeric position inside the node to collapse the cursor here if possible.
452 */
453 
454Xinha.prototype.selectNodeContents = function(node, pos)
455{
456  this.focusEditor();
457  this.forceRedraw();
458  var range;
459  var collapsed = typeof pos == "undefined" ? true : false;
460  // Tables and Images get selected as "objects" rather than the text contents
461  if ( collapsed && node.tagName && node.tagName.toLowerCase().match(/table|img|input|select|textarea/) )
462  {
463    range = this._doc.body.createControlRange();
464    range.add(node);
465  }
466  else
467  {
468    range = this._doc.body.createTextRange();
469    range.moveToElementText(node);
470    //(collapsed) && range.collapse(pos);
471  }
472  range.select();
473};
474 
475/** Insert HTML at the current position, deleting the selection if any.
476 * 
477 *  @param html string
478 */
479 
480Xinha.prototype.insertHTML = function(html)
481{
482  this.focusEditor();
483  var sel = this.getSelection();
484  var range = this.createRange(sel);
485  range.pasteHTML(html);
486};
487
488
489/** Get the HTML of the current selection.  HTML returned has not been passed through outwardHTML.
490 *
491 * @returns string
492 */
493 
494Xinha.prototype.getSelectedHTML = function()
495{
496  var sel = this.getSelection();
497  if (this.selectionEmpty(sel)) return '';
498  var range = this.createRange(sel);
499 
500  // Need to be careful of control ranges which won't have htmlText
501  if( range.htmlText )
502  {
503    return range.htmlText;
504  }
505  else if(range.length >= 1)
506  {
507    return range.item(0).outerHTML;
508  }
509 
510  return '';
511};
512 
513/** Get a Selection object of the current selection.  Note that selection objects are browser specific.
514 *
515 * @returns Selection
516 */
517 
518Xinha.prototype.getSelection = function()
519{
520  return this._doc.selection;
521};
522
523/** Create a Range object from the given selection.  Note that range objects are browser specific.
524 *
525 *  @param sel Selection object (see getSelection)
526 *  @returns Range
527 */
528 
529Xinha.prototype.createRange = function(sel)
530{
531  if (!sel) sel = this.getSelection();
532  return sel.createRange();
533};
534
535/** Determine if the given event object is a keydown/press event.
536 *
537 *  @param event Event
538 *  @returns true|false
539 */
540 
541Xinha.prototype.isKeyEvent = function(event)
542{
543  return event.type == "keydown";
544}
545
546/** Return the character (as a string) of a keyEvent  - ie, press the 'a' key and
547 *  this method will return 'a', press SHIFT-a and it will return 'A'.
548 *
549 *  @param   keyEvent
550 *  @returns string
551 */
552                                   
553Xinha.prototype.getKey = function(keyEvent)
554{
555  return String.fromCharCode(keyEvent.keyCode);
556}
557
558
559/** Return the HTML string of the given Element, including the Element.
560 *
561 * @param element HTML Element DomNode
562 * @returns string
563 */
564 
565Xinha.getOuterHTML = function(element)
566{
567  return element.outerHTML;
568};
569
570// Control character for retaining edit location when switching modes
571Xinha.prototype.cc = String.fromCharCode(0x2009);
572
573Xinha.prototype.setCC = function ( target )
574{
575  var cc = this.cc;
576  if ( target == "textarea" )
577  {
578    var ta = this._textArea;
579    var pos = document.selection.createRange();
580    pos.collapse();
581    pos.text = cc;
582    var index = ta.value.indexOf( cc );
583    var before = ta.value.substring( 0, index );
584    var after  = ta.value.substring( index + cc.length , ta.value.length );
585   
586    if ( after.match(/^[^<]*>/) ) // make sure cursor is in an editable area (outside tags, script blocks, entities, and inside the body)
587    {
588      var tagEnd = after.indexOf(">") + 1;
589      ta.value = before + after.substring( 0, tagEnd ) + cc + after.substring( tagEnd, after.length );
590    }
591    else ta.value = before + cc + after;
592    ta.value = ta.value.replace(new RegExp ('(&[^'+cc+']*?)('+cc+')([^'+cc+']*?;)'), "$1$3$2");
593    ta.value = ta.value.replace(new RegExp ('(<script[^>]*>[^'+cc+']*?)('+cc+')([^'+cc+']*?<\/script>)'), "$1$3$2");
594    ta.value = ta.value.replace(new RegExp ('^([^'+cc+']*)('+cc+')([^'+cc+']*<body[^>]*>)(.*?)'), "$1$3$2$4");
595  }
596  else
597  {
598    var sel = this.getSelection();
599    var r = sel.createRange();
600    if ( sel.type == 'Control' )
601    {
602      var control = r.item(0);
603      control.outerHTML += cc;
604    }
605    else
606    {
607      r.collapse();
608      r.text = cc;
609    }
610  }
611};
612
613Xinha.prototype.findCC = function ( target )
614{
615  var findIn = ( target == 'textarea' ) ? this._textArea : this._doc.body;
616  range = findIn.createTextRange();
617  // in case the cursor is inside a link automatically created from a url
618  // the cc also appears in the url and we have to strip it out additionally
619  if( range.findText( escape(this.cc) ) )
620  {
621    range.select();
622    range.text = '';
623  }
624  if( range.findText( this.cc ) )
625  {
626    range.select();
627    range.text = '';
628  }
629  if ( target == 'textarea' ) this._textArea.focus();
630};
631
632/** Return a doctype or empty string depending on whether the document is in Qirksmode or Standards Compliant Mode
633 *  It's hardly possible to detect the actual doctype without unreasonable effort, so we set HTML 4.01 just to trigger the rendering mode
634 *
635 * @param doc DOM element document
636 * @returns string doctype || empty
637 */
638Xinha.getDoctype = function (doc)
639{
640  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">' : '';
641};
Note: See TracBrowser for help on using the repository browser.