source: trunk/modules/Gecko/paraHandlerBest.js @ 1335

Last change on this file since 1335 was 1335, checked in by gogo, 10 months ago

#1226 - Gecko problems with paraHandlerBest

I actually checked the existing paraHandlerBest in FF 58 to see if I could reproduce the problem still and I couldn't so maybe FF fixed it anyway in the 7 years since it was first reported.

But despite that I'm committing the paraHandlerBest.js contributed by ejucovy and douglas, since it seems to work just as well and they probably actually use Firefox, so, yeah.

  • Property svn:eol-style set to native
  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
File size: 62.9 KB
Line 
1// tabs 2
2
3/**
4* @fileoverview By Adam Wright, for The University of Western Australia
5*
6* Distributed under the same terms as Xinha itself.
7* This notice MUST stay intact for use (see license.txt).
8*
9* Heavily modified by Yermo Lamers of DTLink, LLC, College Park, Md., USA.
10* For more info see http://www.areaedit.com
11*/
12
13/**
14* plugin Info
15*/
16
17EnterParagraphs._pluginInfo =
18{
19  name          : "EnterParagraphs",
20  version       : "1.0",
21  developer     : "Adam Wright",
22  developer_url : "http://www.hipikat.org/",
23  sponsor       : "The University of Western Australia",
24  sponsor_url   : "http://www.uwa.edu.au/",
25  license       : "htmlArea"
26};
27
28// ------------------------------------------------------------------
29
30// "constants"
31
32// Set up the node type constants for browsers that don't support them.
33var ELEMENT_NODE = ELEMENT_NODE || 1;
34var ATTRIBUTE_NODE = ATTRIBUTE_NODE || 2;
35var TEXT_NODE = TEXT_NODE || 3;
36var COMMENT_NODE = COMMENT_NODE || 8;
37var DOCUMENT_NODE = DOCUMENT_NODE || 9;
38/**
39* Whitespace Regex
40*/
41
42EnterParagraphs.prototype._whiteSpace = /^\s*$/;
43
44/**
45* The pragmatic list of which elements a paragraph may not contain
46*/
47
48EnterParagraphs.prototype._pExclusions = /^(address|blockquote|body|dd|div|dl|dt|fieldset|form|h1|h2|h3|h4|h5|h6|hr|li|noscript|ol|p|pre|table|ul)$/i;
49
50/**
51* elements which may contain a paragraph
52*/
53
54EnterParagraphs.prototype._pContainers = /^(body|del|div|fieldset|form|ins|map|noscript|object|td|th)$/i;
55EnterParagraphs.prototype._pWrapper = /^(body|d[ltd]|table|[uo]l|div|p|h[1-6]|li|t[hrd]|del|fieldset|ins|form|map|noscript|object|address|blockquote|pre)$/i;
56
57/**
58* Elements which may not contain paragraphs, and would prefer a break to being split
59*/
60
61EnterParagraphs.prototype._pBreak = /^(address|pre|blockquote)$/i;
62
63/**
64* Elements which may not contain children
65*/
66
67EnterParagraphs.prototype._permEmpty = /^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i;
68
69/**
70* Elements which count as content, as distinct from whitespace or containers
71*/
72
73EnterParagraphs.prototype._elemSolid = /^(applet|br|button|hr|img|input|table)$/i;
74
75/**
76* When the cursor is at the inside edge of one of these elements, we will move the cursor just outside the element, and insert a P element there.
77*/
78
79EnterParagraphs.prototype._pifySibling = /^(address|blockquote|del|div|dl|fieldset|form|h1|h2|h3|h4|h5|h6|hr|ins|map|noscript|object|ol|p|pre|table|ul|)$/i;
80EnterParagraphs.prototype._pifyForced = /^(ul|ol|dl|table)$/i;
81
82/**
83* When the cursor is at the inside edge of one of these elements, and this element's outside edge is just at the inside edge of its immediate parent,
84* we will move the cursor to the outside edge of the immediate parent, and insert a P element there.
85*/
86
87EnterParagraphs.prototype._pifyParent = /^(dd|dt|li|td|th|tr)$/i;
88
89// ---------------------------------------------------------------------
90
91/**
92* EnterParagraphs Constructor
93*/
94
95function EnterParagraphs(editor)
96{
97 
98  this.editor = editor;
99 
100  // hook into the event handler to intercept key presses if we are using
101  // gecko (Mozilla/FireFox)
102  if (Xinha.is_gecko)
103  {
104    this.onKeyPress = this.__onKeyPress;
105  }
106 
107}       // end of constructor.
108
109// ------------------------------------------------------------------
110
111/**
112* name member for debugging
113*
114* This member is used to identify objects of this class in debugging
115* messages.
116*/
117EnterParagraphs.prototype.name = "EnterParagraphs";
118
119/**
120* Gecko's a bit lacking in some odd ways...
121*/
122EnterParagraphs.prototype.insertAdjacentElement = function(ref,pos,el)
123{
124  if ( pos == 'BeforeBegin' )
125  {
126    ref.parentNode.insertBefore(el,ref);
127  }
128  else if ( pos == 'AfterEnd' )
129  {
130    ref.nextSibling ? ref.parentNode.insertBefore(el,ref.nextSibling) : ref.parentNode.appendChild(el);
131  }
132  else if ( pos == 'AfterBegin' && ref.firstChild )
133  {
134    ref.insertBefore(el,ref.firstChild);
135  }
136  else if ( pos == 'BeforeEnd' || pos == 'AfterBegin' )
137  {
138    ref.appendChild(el);
139  }
140 
141};      // end of insertAdjacentElement()
142
143// ----------------------------------------------------------------
144
145/**
146* Passes a global parent node or document fragment to forEachNode
147*
148* @param root node root node to start search from.
149* @param mode string function to apply to each node.
150* @param direction string traversal direction "ltr" (left to right) or "rtl" (right_to_left)
151* @param init boolean
152*/
153
154EnterParagraphs.prototype.forEachNodeUnder = function ( root, mode, direction, init )
155{
156 
157  // Identify the first and last nodes to deal with
158  var start, end;
159 
160  // nodeType 11 is DOCUMENT_FRAGMENT_NODE which is a container.
161  if ( root.nodeType == 11 && root.firstChild )
162  {
163    start = root.firstChild;
164    end = root.lastChild;
165  }
166  else
167  {
168    start = end = root;
169  }
170  // traverse down the right hand side of the tree getting the last child of the last
171  // child in each level until we reach bottom.
172  while ( end.lastChild )
173  {
174    end = end.lastChild;
175  }
176 
177  return this.forEachNode( start, end, mode, direction, init);
178 
179};      // end of forEachNodeUnder()
180
181// -----------------------------------------------------------------------
182
183/**
184* perform a depth first descent in the direction requested.
185*
186* @param left_node node "start node"
187* @param right_node node "end node"
188* @param mode string function to apply to each node. cullids or emptyset.
189* @param direction string traversal direction "ltr" (left to right) or "rtl" (right_to_left)
190* @param init boolean or object.
191*/
192
193EnterParagraphs.prototype.forEachNode = function (left_node, right_node, mode, direction, init)
194{
195 
196  // returns "Brother" node either left or right.
197  var getSibling = function(elem, direction)
198        {
199    return ( direction == "ltr" ? elem.nextSibling : elem.previousSibling );
200        };
201 
202  var getChild = function(elem, direction)
203        {
204    return ( direction == "ltr" ? elem.firstChild : elem.lastChild );
205        };
206 
207  var walk, lookup, fnReturnVal;
208 
209  // FIXME: init is a boolean in the emptyset case and an object in
210  // the cullids case. Used inconsistently.
211 
212  var next_node = init;
213 
214  // used to flag having reached the last node.
215 
216  var done_flag = false;
217 
218  // loop ntil we've hit the last node in the given direction.
219  // if we're going left to right that's the right_node and visa-versa.
220 
221  while ( walk != direction == "ltr" ? right_node : left_node )
222  {
223   
224    // on first entry, walk here is null. So this is how
225    // we prime the loop with the first node.
226   
227    if ( !walk )
228    {
229      walk = direction == "ltr" ? left_node : right_node;
230    }
231    else
232    {
233     
234      // is there a child node?
235     
236      if ( getChild(walk,direction) )
237      {
238       
239        // descend down into the child.
240       
241        walk = getChild(walk,direction);
242       
243      }
244      else
245      {
246       
247        // is there a sibling node on this level?
248       
249        if ( getSibling(walk,direction) )
250        {
251          // move to the sibling.
252          walk = getSibling(walk,direction);
253        }
254        else
255        {
256          lookup = walk;
257         
258          // climb back up the tree until we find a level where we are not the end
259          // node on the level (i.e. that we have a sibling in the direction
260            // we are searching) or until we reach the end.
261         
262          while ( !getSibling(lookup,direction) && lookup != (direction == "ltr" ? right_node : left_node) )
263          {
264            lookup = lookup.parentNode;
265          }
266         
267          // did we find a level with a sibling?
268         
269          // walk = ( lookup.nextSibling ? lookup.nextSibling : lookup ) ;
270         
271          walk = ( getSibling(lookup,direction) ? getSibling(lookup,direction) : lookup ) ;
272         
273        }
274      }
275     
276    }   // end of else walk.
277   
278    // have we reached the end? either as a result of the top while loop or climbing
279    // back out above.
280   
281    done_flag = (walk==( direction == "ltr" ? right_node : left_node));
282   
283    // call the requested function on the current node. Functions
284    // return an array.
285    //
286    // Possible functions are _fenCullIds, _fenEmptySet
287    //
288    // The situation is complicated by the fact that sometimes we want to
289    // return the base node and sometimes we do not.
290    //
291    // next_node can be an object (this.takenIds), a node (text, el, etc) or false.
292   
293    switch( mode )
294    {
295     
296    case "cullids":
297     
298      fnReturnVal = this._fenCullIds(walk, next_node );
299      break;
300     
301    case "find_fill":
302     
303      fnReturnVal = this._fenEmptySet(walk, next_node, mode, done_flag);
304      break;
305     
306    case "find_cursorpoint":
307     
308      fnReturnVal = this._fenEmptySet(walk, next_node, mode, done_flag);
309      break;
310     
311    }
312   
313    // If this node wants us to return, return next_node
314   
315    if ( fnReturnVal[0] )
316    {
317      return fnReturnVal[1];
318    }
319   
320    // are we done with the loop?
321   
322    if ( done_flag )
323    {
324      break;
325    }
326   
327    // Otherwise, pass to the next node
328   
329    if ( fnReturnVal[1] )
330    {
331      next_node = fnReturnVal[1];
332    }
333   
334  }     // end of while loop
335 
336  return false;
337 
338};      // end of forEachNode()
339
340// -------------------------------------------------------------------
341
342/**
343* Find a post-insertion node, only if all nodes are empty, or the first content
344*
345* @param node node current node beinge examined.
346* @param next_node node next node to be examined.
347* @param node string "find_fill" or "find_cursorpoint"
348* @param last_flag boolean is this the last node?
349*/
350
351EnterParagraphs.prototype._fenEmptySet = function( node, next_node, mode, last_flag)
352{
353 
354  // Mark this if it's the first base
355 
356  if ( !next_node && !node.firstChild )
357  {
358    next_node = node;
359  }
360 
361  // Is it an element node and is it considered content? (br, hr, etc)
362  // or is it a text node that is not just whitespace?
363  // or is it not an element node and not a text node?
364 
365  if ( (node.nodeType == 1 && this._elemSolid.test(node.nodeName)) ||
366    (node.nodeType == 3 && !this._whiteSpace.test(node.nodeValue)) ||
367  (node.nodeType != 1 && node.nodeType != 3) )
368  {
369   
370    switch( mode )
371    {
372     
373    case "find_fill":
374     
375      // does not return content.
376     
377      return new Array(true, false );
378      break;
379     
380    case "find_cursorpoint":
381     
382      // returns content
383     
384      return new Array(true, node );
385      break;
386     
387    }
388   
389  }
390 
391  // In either case (fill or findcursor) we return the base node. The avoids
392  // problems in terminal cases (beginning or end of document or container tags)
393 
394  if ( last_flag )
395  {
396    return new Array( true, next_node );
397  }
398 
399  return new Array( false, next_node );
400 
401};      // end of _fenEmptySet()
402
403// ------------------------------------------------------------------------------
404
405/**
406* remove duplicate Id's.
407*
408* @param ep_ref enterparagraphs reference to enterparagraphs object
409*/
410
411EnterParagraphs.prototype._fenCullIds = function ( ep_ref, node, pong )
412{
413 
414  // Check for an id, blast it if it's in the store, otherwise add it
415 
416  if ( node.id )
417  {
418   
419    pong[node.id] ? node.id = '' : pong[node.id] = true;
420  }
421 
422  return new Array(false,pong);
423 
424};
425
426// ---------------------------------------------------------------------------------
427
428/**
429* Grabs a range suitable for paragraph stuffing
430*
431* @param rng Range
432* @param search_direction string "left" or "right"
433*
434* @todo check blank node issue in roaming loop.
435*/
436
437EnterParagraphs.prototype.processSide = function( rng, search_direction)
438{
439 
440  var next = function(element, search_direction) {
441    return ( search_direction == "left" ? element.previousSibling : element.nextSibling );
442  };
443 
444  var node = search_direction == "left" ? rng.startContainer : rng.endContainer;
445  var offset = search_direction == "left" ? rng.startOffset : rng.endOffset;
446  var roam, start = node;
447 
448  // Never start with an element, because then the first roaming node might
449  // be on the exclusion list and we wouldn't know until it was too late
450 
451  while ( start.nodeType == 1 && !this._permEmpty.test(start.nodeName) ) {
452    start = ( offset ? start.lastChild : start.firstChild );
453  }
454 
455  // Climb the tree, left or right, until our course of action presents itself
456  //
457  // if roam is NULL try start.
458  // if roam is NOT NULL, try next node in our search_direction
459  // If that node is NULL, get our parent node.
460  //
461  // If all the above turns out NULL end the loop.
462  //
463  // FIXME: gecko (firefox 1.0.3) - enter "test" into an empty document and press enter.
464  // sometimes this loop finds a blank text node, sometimes it doesn't.
465 
466  roam = roam ? ( next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode ) : start;
467  while (roam) {
468
469    // next() is an inline function defined above that returns the next node depending
470    // on the direction we're searching.
471
472    if ( next(roam,search_direction) ) {
473
474      // If the next sibling's on the exclusion list, stop before it
475
476      if ( this._pExclusions.test(next(roam,search_direction).nodeName) ) {
477
478        return this.processRng(rng, search_direction, roam, next(roam,search_direction), (search_direction == "left"?'AfterEnd':'BeforeBegin'), true, false);
479      }
480    } else {
481
482      // If our parent's on the container list, stop inside it
483
484      if (this._pContainers.test(roam.parentNode.nodeName)) {
485
486        return this.processRng(rng, search_direction, roam, roam.parentNode, (search_direction == "left"?'AfterBegin':'BeforeEnd'), true, false);
487      } else if (this._pExclusions.test(roam.parentNode.nodeName)) {
488
489        // chop without wrapping
490
491        if (this._pBreak.test(roam.parentNode.nodeName)) {
492
493          return this.processRng(rng, search_direction, roam, roam.parentNode,
494            (search_direction == "left"?'AfterBegin':'BeforeEnd'), false, (search_direction == "left" ?true:false));
495        } else {
496
497          // the next(roam,search_direction) in this call is redundant since we know it's false
498          // because of the "if next(roam,search_direction)" above.
499          //
500          // the final false prevents this range from being wrapped in <p>'s most likely
501          // because it's already wrapped.
502
503          return this.processRng(rng,
504            search_direction,
505            (roam = roam.parentNode),
506            (next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode),
507            (next(roam,search_direction) ? (search_direction == "left"?'AfterEnd':'BeforeBegin') : (search_direction == "left"?'AfterBegin':'BeforeEnd')),
508            false,
509            false);
510        }
511      }
512    }
513    roam = roam ? ( next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode ) : start;
514  }
515 
516};      // end of processSide()
517
518// ------------------------------------------------------------------------------
519
520/**
521* processRng - process Range.
522*
523* Neighbour and insertion identify where the new node, roam, needs to enter
524* the document; landmarks in our selection will be deleted before insertion
525*
526* @param rn Range original selected range
527* @param search_direction string Direction to search in.
528* @param roam node
529* @param insertion string may be AfterBegin of BeforeEnd
530* @return array
531*/
532
533EnterParagraphs.prototype.processRng = function(rng, search_direction, roam, neighbour, insertion, pWrap, preBr)
534{
535  var node = search_direction == "left" ? rng.startContainer : rng.endContainer;
536  var offset = search_direction == "left" ? rng.startOffset : rng.endOffset;
537 
538  // Define the range to cut, and extend the selection range to the same boundary
539 
540  var editor = this.editor;
541  var newRng = editor._doc.createRange();
542 
543  newRng.selectNode(roam);
544  // extend the range in the given direction.
545 
546  if ( search_direction == "left")
547  {
548    newRng.setEnd(node, offset);
549    rng.setStart(newRng.startContainer, newRng.startOffset);
550  }
551  else if ( search_direction == "right" )
552  {
553   
554    newRng.setStart(node, offset);
555    rng.setEnd(newRng.endContainer, newRng.endOffset);
556  }
557  // Clone the range and remove duplicate ids it would otherwise produce
558 
559  var cnt = newRng.cloneContents();
560 
561  // in this case "init" is an object not a boolen.
562 
563  this.forEachNodeUnder( cnt, "cullids", "ltr", this.takenIds, false, false);
564 
565  // Special case, for inserting paragraphs before some blocks when caret is at
566  // their zero offset.
567  //
568  // Used to "open up space" in front of a list, table. Usefull if the list is at
569  // the top of the document. (otherwise you'd have no way of "moving it down").
570 
571  var pify, pifyOffset, fill;
572  pify = search_direction == "left" ? (newRng.endContainer.nodeType == 3 ? true:false) : (newRng.startContainer.nodeType == 3 ? false:true);
573  pifyOffset = pify ? newRng.startOffset : newRng.endOffset;
574  pify = pify ? newRng.startContainer : newRng.endContainer;
575 
576  if ( this._pifyParent.test(pify.nodeName) && pify.parentNode.childNodes.item(0) == pify )
577  {
578    while ( !this._pifySibling.test(pify.nodeName) )
579    {
580      pify = pify.parentNode;
581    }
582  }
583 
584  // NODE TYPE 11 is DOCUMENT_FRAGMENT NODE
585  // I do not profess to understand any of this, simply applying a patch that others say is good - ticket:446
586  if ( cnt.nodeType == 11 && !cnt.firstChild)
587  {     
588    if (pify.nodeName != "BODY" || (pify.nodeName == "BODY" && pifyOffset != 0))
589    { //WKR: prevent body tag in empty doc
590      cnt.appendChild(editor._doc.createElement(pify.nodeName));
591    }
592  }
593 
594  // YmL: Added additional last parameter for fill case to work around logic
595  // error in forEachNode()
596 
597  fill = this.forEachNodeUnder(cnt, "find_fill", "ltr", false );
598 
599  if ( fill &&
600    this._pifySibling.test(pify.nodeName) &&
601  ( (pifyOffset == 0) || ( pifyOffset == 1 && this._pifyForced.test(pify.nodeName) ) ) )
602  {
603   
604    roam = editor._doc.createElement( 'p' );
605    roam.innerHTML = "&nbsp;";
606   
607    // roam = editor._doc.createElement('p');
608    // roam.appendChild(editor._doc.createElement('br'));
609   
610    // for these cases, if we are processing the left hand side we want it to halt
611    // processing instead of doing the right hand side. (Avoids adding another <p>&nbsp</p>
612      // after the list etc.
613     
614      if ((search_direction == "left" ) && pify.previousSibling)
615      {
616       
617        return new Array(pify.previousSibling, 'AfterEnd', roam);
618      }
619      else if (( search_direction == "right") && pify.nextSibling)
620      {
621       
622        return new Array(pify.nextSibling, 'BeforeBegin', roam);
623      }
624      else
625      {
626       
627        return new Array(pify.parentNode, (search_direction == "left"?'AfterBegin':'BeforeEnd'), roam);
628      }
629     
630  }
631 
632  // If our cloned contents are 'content'-less, shove a break in them
633 
634  if ( fill )
635  {
636   
637    // Ill-concieved?
638    //
639    // 3 is a TEXT node and it should be empty.
640    //
641   
642    if ( fill.nodeType == 3 )
643    {
644      // fill = fill.parentNode;
645     
646      fill = editor._doc.createDocumentFragment();
647    }
648   
649    if ( (fill.nodeType == 1 && !this._elemSolid.test()) || fill.nodeType == 11 )
650    {
651     
652      // FIXME:/CHECKME: When Xinha is switched from WYSIWYG to text mode
653      // Xinha.getHTMLWrapper() will strip out the trailing br. Not sure why.
654     
655      // fill.appendChild(editor._doc.createElement('br'));
656     
657      var pterminator = editor._doc.createElement( 'p' );
658      pterminator.innerHTML = "&nbsp;";
659     
660      fill.appendChild( pterminator );
661     
662    }
663    else
664    {
665     
666      // fill.parentNode.insertBefore(editor._doc.createElement('br'),fill);
667     
668      var pterminator = editor._doc.createElement( 'p' );
669      pterminator.innerHTML = "&nbsp;";
670     
671      fill.parentNode.insertBefore(parentNode,fill);
672     
673    }
674  }
675 
676  // YmL: If there was no content replace with fill
677  // (previous code did not use fill and we ended up with the
678    // <p>test</p><p></p> because Gecko was finding two empty text nodes
679    // when traversing on the right hand side of an empty document.
680   
681    if ( fill )
682    {
683     
684      roam = fill;
685    }
686    else
687    {
688      // And stuff a shiny new object with whatever contents we have
689     
690      roam = (pWrap || (cnt.nodeType == 11 && !cnt.firstChild)) ? editor._doc.createElement('p') : editor._doc.createDocumentFragment();
691      roam.appendChild(cnt);
692    }
693   
694    if (preBr)
695    {
696      roam.appendChild(editor._doc.createElement('br'));
697    }
698    // Return the nearest relative, relative insertion point and fragment to insert
699   
700    return new Array(neighbour, insertion, roam);
701   
702};      // end of processRng()
703
704// ----------------------------------------------------------------------------------
705
706/**
707* are we an <li> that should be handled by the browser?
708*
709* there is no good way to "get out of" ordered or unordered lists from Javascript.
710* We have to pass the onKeyPress 13 event to the browser so it can take care of
711* getting us "out of" the list.
712*
713* The Gecko engine does a good job of handling all the normal <li> cases except the "press
714* enter at the first position" where we want a <p>&nbsp</p> inserted before the list. The
715* built-in behavior is to open up a <li> before the current entry (not good).
716*
717* @param rng Range range.
718*/
719
720EnterParagraphs.prototype.isNormalListItem = function(rng)
721{
722 
723  var node, listNode;
724 
725  node = rng.startContainer;
726 
727  if (( typeof node.nodeName != 'undefined') &&
728    ( node.nodeName.toLowerCase() == 'li' ))
729  {
730   
731    // are we a list item?
732   
733    listNode = node;
734  }
735  else if (( typeof node.parentNode != 'undefined' ) &&
736    ( typeof node.parentNode.nodeName != 'undefined' ) &&
737  ( node.parentNode.nodeName.toLowerCase() == 'li' ))
738  {
739   
740    // our parent is a list item.
741   
742    listNode = node.parentNode;
743   
744  }
745  else
746  {
747    // neither we nor our parent are a list item. this is not a normal
748    // li case.
749   
750    return false;
751  }
752 
753  // at this point we have a listNode. Is it the first list item?
754 
755  if ( ! listNode.previousSibling )
756  {
757    // are we on the first character of the first li?
758   
759    if ( rng.startOffset == 0 )
760    {
761      return false;
762    }
763  }
764  return true;
765 
766};      // end of isNormalListItem()
767
768// ----------------------------------------------------------------------------------
769/**
770* Called when a key is pressed in the editor
771*/
772
773EnterParagraphs.prototype.__onKeyPress = function(ev)
774{
775 
776  // If they've hit enter and shift is not pressed, handle it
777 
778  if (ev.keyCode == 13 && !ev.shiftKey && this.editor._iframe.contentWindow.getSelection)
779  {
780    return this.breakLine(ev, this.editor._doc);
781  }
782 
783};      // end of _onKeyPress()
784
785// -----------------------------------------------------------------------------------
786
787/**
788* Helper function to find the index of the given node with its parent's
789* childNodes array.  If there is any problem with the lookup, we'll return
790* NULL.
791*/
792
793EnterParagraphs.prototype.indexInParent = function (el)
794{
795  if (!el.parentNode || !el.parentNode.childNodes)
796  {
797    // The element is at the root of the tree, or it's a broken node.
798    return null;
799  }
800
801  for (var index=0; index<el.parentNode.childNodes.length; ++index)
802  {
803    if (el == el.parentNode.childNodes[index])
804    {
805      return index;
806    }
807  }
808
809  // This will only happen if the DOM node is broken...
810  return null;
811}
812
813/*
814* Determine if a cursor points to the end of it's containing node.
815*/
816EnterParagraphs.prototype.cursorAtEnd = function (cursorNode, cursorOffset)
817{
818  if (cursorNode.nodeType == TEXT_NODE)
819  {
820    if (cursorOffset == cursorNode.nodeValue.length)
821    {
822      return true;
823    }
824    // We're in the middle of a text node.  If the node is a whitespace node,
825    // we'll ignore it and treat it as if the cursor were after the node, and
826    // not in it.
827    if (/\S/.test(cursorNode.nodeValue))
828    {
829      return false;
830    }
831    cursorOffset = this.indexInParent(cursorNode) + 1;
832    cursorNode = cursorNode.parentNode;
833   
834    // We need to make sure we there wasn't an error in indexInParent
835    if (cursorOffset === null)
836    {
837      return false;
838    }
839  }
840  // The easy case, it's after the last node...
841  if (cursorOffset == cursorNode.childNodes.length)
842  {
843    return true;
844  }
845  // At this point, if the pointed to node is a whitespace node, and all of
846  // it's nextSiblings are also whitespace node, then the cursor is at the end
847  // of the node.
848  for (var node = cursorNode.childNodes[cursorOffset]; node; node = node.nextSibling)
849  {
850    if ((node.nodeType != TEXT_NODE) || (/\S/.test(node.nodeValue)))
851    {
852      return false;
853    }
854  }
855  return true;
856}
857/*
858* Test suite for this, because it's really tough to get right.
859*/
860EnterParagraphs.RunTests = function(xinha, debug)
861{
862  var test = function(message, before, cursorBefore, after, cursorAfter, cursorAfter2) {
863    console.group('Test: ', message);
864    if (before !== null) {
865      xinha.setHTML(before);
866    }
867    // Do something
868    var cAnchor, cOffset;
869
870    var mockEvent = {
871      preventDefault: function() {if (debug) console.log("Preventing default.");},
872      stopPropagation: function() {if (debug) console.log("Stopping propagation.");},
873    }
874    function setCursor(commands) {
875      cAnchor = xinha._doc.body;
876      cOffset = 0;
877      try {
878        for (var index=0; index<commands.length; ++index) {
879          var command = commands[index];
880          if ('id' == command[0]) {
881              cAnchor = xinha._doc.getElementById(command[1]);
882          } else if ('firsttag' == command[0]) {
883              cAnchor = xinha._doc.getElementsByTagName(command[1])[0];
884          } else if ('child' == command[0]) {
885              cAnchor = cAnchor.childNodes[command[1]];
886          } else if ('next' == command[0]) {
887            for (var next=command[1]; next > 0 && cAnchor.nextSibling; --next) {
888              cAnchor = cAnchor.nextSibling;
889            }
890          } else if ('previous' == command[0]) {
891            for (var previous=command[1]; previous > 0 && cAnchor.previousSibling; --previous) {
892              cAnchor = cAnchor.previousSibling;
893            }
894          } else if ('offset' == command[0]) {
895            if (command[1] == 'length') {
896              if (TEXT_NODE == cAnchor.nodeType) {
897                cOffset = cAnchor.nodeValue.length;
898              } else {
899                cOffset = cAnchor.childNodes.length;
900              }
901            } else if (command[1] < 0) {
902              if (TEXT_NODE == cAnchor.nodeType) {
903                cOffset = cAnchor.nodeValue.length + command[1];
904              } else {
905                cOffset = cAnchor.childNodes.length + command[1];
906              }
907            } else {
908              cOffset = command[1];
909            }
910          }
911        }
912      } catch(e) {
913        cAnchor = null;
914        cOffset = null;
915      }
916    }
917
918    setCursor(cursorBefore);
919
920    var selection = xinha.getSelection();
921    var range = xinha.createRange(selection);
922
923    range.setStart(cAnchor, cOffset);
924    range.setEnd(cAnchor, cOffset);
925    selection.removeAllRanges();
926    selection.addRange(range);
927
928    // Breakline
929    try {
930      xinha.plugins['EnterParagraphs'].instance.breakLine(mockEvent, xinha._doc);
931    } catch (e) {
932      console.error('Breakline threw exception ', e);
933      console.groupEnd();
934      return;
935    }
936
937    var selection = xinha.getSelection();
938    var range = xinha.createRange(selection);
939
940    setCursor(cursorAfter);
941
942    if ((selection.anchorNode != cAnchor) || (selection.anchorOffset != cOffset)) {
943      // Sometimes there are multiple equivalent selection, let's see if we received alternatives.
944      if (typeof cursorAfter2 != 'undefined') {
945        setCursor(cursorAfter2);
946        if ((selection.anchorNode != cAnchor) || (selection.anchorOffset != cOffset)) {
947          console.error('Actual anchor: ' + selection.anchorNode +
948                       '\nActual offset: ' + selection.anchorOffset +
949                       '\nExpected anchor: ' + cAnchor +
950                       '\nExpected offset: ' + cOffset);
951        }
952      } else {
953        console.error('Actual anchor: ' + selection.anchorNode +
954                     '\nActual offset: ' + selection.anchorOffset +
955                     '\nExpected anchor: ' + cAnchor +
956                     '\nExpected offset: ' + cOffset);
957      }
958    }
959
960    result = xinha.getInnerHTML();
961    if (result.trim() == after.trim()) {
962      console.info('Success!');
963    } else {
964      console.error('Was: \n`' + before +
965                   '`\nExpected: \n`' + after +
966                   '`\nGot: \n`' + result + '`');
967    }
968    console.groupEnd();
969  }
970  contentBackup = xinha.getInnerHTML();
971xinha.setHTML("");
972  console.group('Running tests:');
973  /*
974     The initial content on browser load seems to be:
975     <body><br />\n</body>
976     That's a break tag and a whitespace text node containing a newline character.
977    */
978  test('Initial Xinha Content',
979       null, [],
980       '<p>&nbsp;</p><p><br></p>\n', [['child', 1]]);  // Mozilla kicks off a trailing newline.  Do I care about this?
981  test('Initial Xinha Content: Recreated',
982       '<br>\n', [],
983       '<p>&nbsp;</p><p><br></p>\n', [['child', 1]]);  // Mozilla kicks off a trailing newline.  Do I care about this?
984
985  test('Empty Body',
986       '', [],
987       '<p>&nbsp;</p><p><br></p>', [['child', 1]],
988                                   [['child', 1], ['child', 0]]);
989
990  test('Text node in body: text node',
991       'Hi', [], // Point to text node
992       '<p>&nbsp;</p><p>Hi</p>', [['child', 1]],
993                                 [['child', 1], ['child', 0]]);
994  test('Text node in body: first char',
995       'Hi', [['child', 0]], // Point to 'H'
996       '<p>&nbsp;</p><p>Hi</p>', [['child', 1]],
997                                 [['child', 1], ['child', 0]]);
998  test('Text node in body: split text',
999       'Hi', [['child', 0], ['offset', 1]], // Point to 'i'
1000       '<p>H</p><p>i</p>', [['child', 1]],
1001                           [['child', 1], ['child', 0]]);
1002  test('Text node in body: after text',
1003       'Hi', [['child', 0], ['offset', 'length']], // Point after 'i'
1004       '<p>Hi</p><p>&nbsp;</p>', [['child', 1]],
1005                                 [['child', 1], ['child', 0]]);
1006  test('Text node in body: after text node',
1007       'Hi', [['offset', 'length']], // Point after text node
1008       'Hi<p>&nbsp;</p>', [['child', 1]],  // This is not ideal output, but the line breaker never sees the text node and
1009                          [['child', 1], ['child', 0]]); // so can't do anything about it.
1010
1011  // For the next two tests, Douglas thinks the ideal output would be (either of):
1012  //     [['child', 1], ['child', 0]],
1013  //     [['child', 1], ['child', 0], ['child', 0]]);
1014  // That is, with the cursor inside the <em> node.
1015  // I (ejucovy) think that the output [['child', 1]], inside the second <p>
1016  // but outside the <em>, is better.  It's also what we're actually getting back
1017  // from the browser, so let's go with that and leave this here for posterity..
1018  test('Body with inline tag: em node',
1019       '<em>hi</em>', [], // Point to document body
1020       '<p>&nbsp;</p><p><em>hi</em></p>', [['child', 1]]);
1021  test('Body with inline em: inside em node',
1022       '<em>hi</em>', [['child', 0]], // Point to beginning of em node (before h)
1023       '<p>&nbsp;</p><p><em>hi</em></p>', [['child', 1]]);
1024
1025  test('Body with inline tag: text node',
1026       '<em>hi</em>', [['child', 0]],
1027       '<p>&nbsp;</p><p><em>hi</em></p>', [['child', 1], ['child', 0]],
1028                                          [['child', 1], ['child', 0], ['child', 0]]);
1029  test('Body with inline tag: first char',
1030       '<em>hi</em>', [['child', 0], ['child', 0]],
1031       '<p>&nbsp;</p><p><em>hi</em></p>', [['child', 1], ['child', 0]],
1032                                          [['child', 1], ['child', 0], ['child', 0]]);
1033  test('Body with inline tag: split text',
1034       '<em>hi</em>', [['child', 0], ['child', 0], ['offset', 1]],
1035       '<p><em>h</em></p><p><em>i</em></p>', [['child', 1], ['child', 0]],
1036                                             [['child', 1], ['child', 0], ['child', 0]]);
1037  test('Body with inline tag: after text',
1038       '<em>hi</em>', [['child', 0], ['child', 0], ['offset', 'length']],
1039       '<p><em>hi</em></p><p>&nbsp;</p>', [['child', 1], ['child', 0]],
1040                                          [['child', 1], ['child', 0], ['child', 0]]);
1041  // I hate that this is expected behavior, but the split code doesn't see the em tag in these two cases.
1042  test('Body with inline tag: after text node',
1043       '<em>hi</em>', [['child', 0], ['offset', 'length']],
1044       '<em>hi</em><p>&nbsp;</p>', [['child', 1], ['child', 0]],
1045                                          [['child', 1], ['child', 0], ['child', 0]]);
1046  test('Body with inline tag: after em node',
1047       '<em>hi</em>', [['offset', 'length']],
1048       '<em>hi</em><p>&nbsp;</p>', [['child', 1], ['child', 0]],
1049                                          [['child', 1], ['child', 0], ['child', 0]]);
1050
1051  /***************  Repeat the header block for each header level once the tests are passing *********************/
1052  test('Split header 1: h1 node',
1053       '<h1>hi</h1>', [],
1054       '<p>&nbsp;</p><h1>hi</h1>', [['child', 1]],
1055                                   [['child', 1], ['child', 0]]);
1056  test('Split header 1: text node',
1057       '<h1>hi</h1>', [['child', 0]],
1058       '<p>&nbsp;</p><h1>hi</h1>', [['child', 1]],
1059                                   [['child', 1], ['child', 0]]);
1060  test('Split header 1: first char',
1061       '<h1>hi</h1>', [['child', 0], ['child', 0]],
1062       '<p>&nbsp;</p><h1>hi</h1>', [['child', 1]],
1063                                   [['child', 1], ['child', 0]]);
1064  test('Split header 1: split text',
1065       '<h1>hi</h1>', [['child', 0], ['child', 0], ['offset', 1]],
1066       '<h1>h</h1><h1>i</h1>', [['child', 1]],
1067                               [['child', 1], ['child', 0]]);
1068  test('Split header 1: after text',
1069       '<h1>hi</h1>', [['child', 0], ['child', 0], ['offset', 'length']],
1070       '<h1>hi</h1><p>&nbsp;</p>', [['child', 1]],
1071                                   [['child', 1], ['child', 0]]);
1072  test('Split header 1: after text node',
1073       '<h1>hi</h1>', [['child', 0], ['offset', 'length']],
1074       '<h1>hi</h1><p>&nbsp;</p>', [['child', 1]],
1075                                   [['child', 1], ['child', 0]]);
1076  test('Split header 1: after h1 node',
1077       '<h1>hi</h1>', [['offset', 'length']],
1078       '<h1>hi</h1><p>&nbsp;</p>', [['child', 1]],
1079                                   [['child', 1], ['child', 0]]);
1080
1081  console.groupEnd();
1082  xinha.setHTML(contentBackup);
1083  // EnterParagraphs.RunTests(xinha_editors['myTextArea'])
1084}
1085/*
1086* Determine if a cursor points to the end of it's containing node.
1087*/
1088EnterParagraphs.prototype.cursorAtBeginning = function (cursorNode, cursorOffset)
1089{
1090  if (cursorOffset == 0)
1091  {
1092    return true;
1093  }
1094  if (cursorNode.nodeType == TEXT_NODE)
1095  {
1096    // We're in the middle of a text node.  If the node is a whitespace node,
1097    // we'll ignore it and treat it as if the cursor were at the beginning of
1098    // the node, and not in it.
1099    if (/\S/.test(cursorNode.nodeValue))
1100    {
1101      return false;
1102    }
1103    cursorOffset = this.indexInParent(cursorNode);
1104    cursorNode = cursorNode.parentNode;
1105   
1106    // We need to make sure we there wasn't an error in indexInParent
1107    if (cursorOffset === null)
1108    {
1109      return false;
1110    }
1111
1112    // We have to check the new offset for the easy case.
1113    if (cursorOffset == 0)
1114    {
1115      return true;
1116    }
1117  }
1118  // At this point, if all of the nodes before the cursor are white space
1119  // nodes, then the cursor is at the beginning of the node.
1120  for (var node = cursorNode.childNodes[cursorOffset-1]; node; node = node.previousSibling)
1121  {
1122    if ((node.nodeType != TEXT_NODE) || (/\S/.test(node.nodeValue)))
1123    {
1124      return false;
1125    }
1126  }
1127  return true;
1128}
1129/**
1130* Handles the pressing of an unshifted enter for Gecko
1131*/
1132
1133EnterParagraphs.prototype.breakLine = function(ev, doc)
1134{
1135  // Helper function that copies a DOM element and its attributes (except the
1136  // id) without any of the contents.
1137  var safeShallowCopy = function(node, doc)
1138  {
1139    var copy = doc.createElement(node.nodeName);
1140    for (var index=0; index < node.attributes.length; ++index)
1141    {
1142      var attr = node.attributes[index];
1143      if ('id' != attr.name.toLowerCase())
1144      {
1145        copy.setAttribute(attr.name, attr.value);
1146      }
1147    }
1148    return copy;
1149  }
1150
1151  // Helper function that will get the node immediately following the current
1152  // node, but without descending into children nodes.  When looking at the
1153  // markup of the document, this means that if a node to the right of this
1154  // node in the text is at a lower depth in the DOM tree, than we will return
1155  // it's first parent that is at our depth our higher in the tree.
1156  var nextRootNode = function(node)
1157  {
1158    if (node.nextSibling)
1159    {
1160      return node.nextSibling;
1161    }
1162    for (var nextRoot = node.parentNode;nextRoot;nextRoot = nextRoot.parentNode)
1163    {
1164      if (nextRoot.nextSibling)
1165      {
1166        return nextRoot.nextSibling;
1167      }
1168    }
1169  }
1170
1171  // A cursor is specified by a node and an offset, so we will split at that
1172  // location.  It should be noted that if splitNode is a text node,
1173  // splitOffset is an offset into the text contents.  If not, it is an index
1174  // into the childNodes array.
1175  var splitTree = function(root, splitNode, splitOffset, doc)
1176  {
1177    // Split root into two.
1178    var breaker = safeShallowCopy(root, doc);
1179    if (root.nextSibling)
1180    {
1181      breaker = root.parentNode.insertBefore(breaker,root.nextSibling);
1182    }
1183    else
1184    {
1185      breaker = root.parentNode.appendChild(breaker);
1186    }
1187
1188    var insertNode = breaker;
1189    // XXX TODO don't use a closure to access this, pass it in...
1190    for (;recreateStack.length>0;)
1191    {
1192      var stackEl = safeShallowCopy(recreateStack.pop(), doc)
1193      insertNode.appendChild(stackEl);
1194      // Move content here
1195      insertNode = stackEl;
1196    }
1197
1198    // We need to keep track of the new cursor location.  When our cursor is in
1199    // the middle of a text node, the new cursor will be at the beginning of
1200    // the text node we create to contain the text to the right of the cursor.
1201    // Otherwise, the cursor will point to a node, and the new cursor needs to
1202    // point to that node in it's new location.
1203    var newCursorNode = null;
1204
1205    var sourceNode = splitNode;
1206    var sourceOffset = splitOffset;
1207    if (TEXT_NODE == sourceNode.nodeType)
1208    {
1209      var textNode = doc.createTextNode(sourceNode.nodeValue.substring(splitOffset,sourceNode.nodeValue.length));
1210      newCursorNode = textNode = insertNode.appendChild(textNode);
1211      sourceNode.nodeValue = sourceNode.nodeValue.substring(0,splitOffset);
1212    }
1213
1214    // When splitting a tree, we need to take any nodes that are after the
1215    // split and move them into their location in the new tree.  We can have
1216    // siblings at each level of the tree, so we need to walk from the inside
1217    // of the source outwards, and move the offending nodes to the equivalent
1218    // position on the newly duplicated tree.
1219
1220    // Move insertNode from the inside outwards towards the root, moving any
1221    // content nodes as we go.  We'll make sure that we can do the same with
1222    // sourceNode
1223    while (insertNode != root.parentNode)
1224    {
1225      for (var moveNode=sourceNode.childNodes[sourceOffset];moveNode;)
1226      {
1227        // We have to take a reference to the next sibling before cutting out
1228        // of the tree, or we will lose our place.
1229
1230        // nextNode can potentially be null.  This is not a problem.
1231        var nextNode = moveNode.nextSibling;
1232        var cutNode = moveNode.parentNode.removeChild(moveNode);
1233        insertNode.appendChild(cutNode);
1234        moveNode = nextNode;
1235      }
1236
1237      // Move both of our node pointers one step closer to the root node.
1238      sourceOffset = EnterParagraphs.prototype.indexInParent(sourceNode);
1239      sourceNode = sourceNode.parentNode;
1240      insertNode = insertNode.parentNode;
1241    }
1242
1243    // Below code needs to check for element node with empty text node.
1244    // An empty node is an text node of zero length or an element node with no
1245    // children, or whose only children are zero-length text nodes.
1246    var emptyNode = function(node)
1247    {
1248      if ((TEXT_NODE == node.nodeType) && (0 == node.nodeValue.length))
1249      {
1250        // Text nodes are empty if there is no text.
1251        return true;
1252      }
1253
1254      if (ELEMENT_NODE == node.nodeType)
1255      {
1256        for (var child = node.firstChild; child; child = child.nextSibling)
1257        {
1258          if ((ELEMENT_NODE == child.nodeType) || (0 != child.nodeValue.length))
1259          {
1260            // If there are any element children, or text nodes with text in
1261            // them, this node is not empty.
1262            return false;
1263          }
1264        }
1265
1266        // node has no childNodes that are elements and no childNodes that are
1267        // text nodes with text in them.
1268        return true;
1269      }
1270
1271      return false;
1272    }
1273
1274    var stuffEmptyNode = function(node, doc)
1275    {
1276      if (!emptyNode(node))
1277      {
1278        return;
1279      }
1280
1281      if (TEXT_NODE == node.nodeType)
1282      {
1283        // Unicode equivalent of non breaking whitespace.
1284        node.nodeValue = '\u00a0';
1285      }
1286      else if (0 == node.childNodes.length)
1287      {
1288        // Unicode equivalent of non breaking whitespace.
1289        node.appendChild(doc.createTextNode('\u00a0'));
1290      }
1291      else
1292      {
1293        // Unicode equivalent of non breaking whitespace. The node is empty,
1294        // but it has child nodes, so firstChild is guaranteed to be an empty
1295        // text node.
1296        node.firstChild.nodeValue = '\u00a0';
1297      }
1298    }
1299
1300    // If the cursor node wasn't created by the split (it was moved), then that
1301    // means we need to point to the inside of our brand new tree.
1302    if (!newCursorNode)
1303    {
1304      newCursorNode = breaker.childNodes[0];
1305    }
1306
1307    // Make sure when we split the tree that we don't leave any empty nodes, as
1308    // that would have visual glitches.
1309    stuffEmptyNode(splitNode, doc);
1310    stuffEmptyNode(newCursorNode, doc);
1311
1312    // So that we can correctly set the selection, we'll return a reference to
1313    // the inserted subtree.
1314    return newCursorNode;
1315  }
1316  var insertLineBreak = function(cursorParent, cursorOffset, useNewline, doc)
1317  {
1318    if (TEXT_NODE == cursorParent.nodeType)
1319    {
1320      // The cursor points inside of a text node, we insert the newline
1321      // directly into the text.
1322      var splitNode = cursorParent;
1323      var splitOffset = cursorOffset;
1324      if (useNewline)
1325      {
1326        splitNode.nodeValue = splitNode.nodeValue.substring(0,splitOffset) + '\n' + splitNode.nodeValue.substring(splitOffset,splitNode.nodeValue.length);
1327      }
1328      else
1329      {
1330        var newTextNode = doc.createTextNode(splitNode.nodeValue.substring(splitOffset,splitNode.nodeValue.length));
1331        var newBreakNode = doc.createElement('br');
1332        splitNode.nodeValue = splitNode.nodeValue.substring(0,splitOffset);
1333
1334        var appendIndex = EnterParagraphs.prototype.indexInParent(cursorParent);
1335        if (appendIndex == cursorParent.parentNode.length-1)
1336        {
1337          newBreakNode = cursorParent.appendChild(newBreakNode);
1338          newTextNode = cursorParent.appendChild(newTextNode);
1339        }
1340        else
1341        {
1342          newTextNode = cursorParent.insertBefore(newTextNode, cursorParent.parentNode.childNodes[appendIndex+1]);
1343          newBreakNode = cursorParent.insertBefore(newBreakNode, newTextNode);
1344        }
1345        return newBreakNode;
1346      }
1347    }
1348    else if (0 == cursorParent.childNodes.length)
1349    {
1350      // The cursor is inside an empty element or document node, so we insert a txt node or break element as necessary.
1351      if (useNewline)
1352      {
1353        var breakingNode = doc.createTextNode('\n');
1354        cursorParent.appendChild(breakingNode);
1355      }
1356      else
1357      {
1358        var breakingNode = doc.createElement('br');
1359        return cursorParent.appendChild(breakingNode);
1360      }
1361    }
1362    else if ((cursorOffset == cursorParent.childNodes.length) && (TEXT_NODE == cursorParent.childNodes[cursorOffset-1].nodeType))
1363    {
1364      // The cursor is at the after the last node, and the previous node is a
1365      // text node where we can insert the newline.
1366      if (useNewline)
1367      {
1368        var lastTextNode = cursorParent.childNodes[cursorOffset-1];
1369        lastTextNode.nodeValue = lastTextNode.nodeValue + '\n';
1370      }
1371      else
1372      {
1373        var breakingNode = doc.createElement('br');
1374        return cursorParent.appendChild(breakingNode);
1375      }
1376    }
1377    else if (cursorOffset == cursorParent.childNodes.length)
1378    {
1379      // The cursor is at the after the last node, and the previous node is an
1380      // not text, so we must insert a text node.
1381      if (useNewline)
1382      {
1383        var breakingNode = doc.createTextNode('\n');
1384        cursorParent.appendChild(breakingNode);
1385      }
1386      else
1387      {
1388        var breakingNode = doc.createElement('br');
1389        return cursorParent.appendChild(breakingNode);
1390      }
1391    }
1392    else if (TEXT_NODE == cursorParent.childNodes[cursorOffset].nodeType)
1393    {
1394      // The cursor points to a text node, insert our newline there.
1395      if (useNewline)
1396      {
1397        var splitNode = cursorParent.childNodes[cursorOffset];
1398        splitNode.nodeValue = '\n' + splitNode.nodeValue;
1399      }
1400      else
1401      {
1402        var breakingNode = doc.createElement('br');
1403        return cursorParent.insertBefore(breakingNode, cursorParent[cursorOffset]);
1404      }
1405    }
1406    else if (TEXT_NODE == cursorParent.childNodes[cursorOffset-1].nodeType)
1407    {
1408      // The cursor points to an non-text node, but there is a text node just
1409      // before where we can insert a newline.
1410      if (useNewline)
1411      {
1412        var splitNode = cursorParent.childNodes[cursorOffset-1];
1413        splitNode.nodeValue = splitNode.nodeValue + '\n';
1414      }
1415      else
1416      {
1417        var breakingNode = doc.createElement('br');
1418        return cursorParent.insertBefore(breakingNode, cursorParent[cursorOffset]);
1419      }
1420    }
1421    else
1422    {
1423      // The cursor points between two non-text nodes, so we must insert a text
1424      // node.
1425      if (useNewline)
1426      {
1427        var breakingNode = doc.createTextNode('\n');
1428        cursorParent.insertBefore(breakingNode, cursorParent.childNodes[cursorOffset]);
1429      }
1430      else
1431      {
1432        var breakingNode = doc.createElement('br');
1433        return cursorParent.insertBefore(breakingNode, cursorParent.childNodes[cursorOffset]);
1434      }
1435    }
1436  }
1437
1438  /* ***********************************************************************
1439                                 CODE
1440     *********************************************************************** */
1441  // In the case of the user pressing enter, we have to break the line somehow.
1442  // If there is anything already selected, we interpret that the user wishes
1443  // for the content to be deleted.
1444 
1445  var selection = this.editor.getSelection();
1446  var range = this.editor.createRange(selection);
1447 
1448  selection.collapseToStart();
1449  range.deleteContents();
1450
1451  // We do some magic manipulation to help with user intent.
1452  this.moveCursorOnEdge(selection);
1453
1454  // Take a reference to the cursor.
1455  var cursorParent = selection.anchorNode;
1456  var cursorOffset = selection.anchorOffset;
1457
1458  // Now that we have an empty selection, the process of breaking the line is a
1459  // bit simpler.  Our strategy for breaking the line is as follows:
1460
1461  // We will modify the cursor position in an attempt to guess the user's
1462  // intent.  When the cursor is at the inside edge of certain elements, we
1463  // work under the assumption that the user wished to select just outside of
1464  // that element. As such, we will move the cursor to just outside the
1465  // element, and then continue.
1466
1467  // Next, we find the first non-inline element that contains our cursor.
1468  // These can be broken into four types:
1469  // 1) Definition lists and their elements (dl, dt, dd)
1470  // 2) Other lists (ul, ol)
1471  // 3) Other containers (body, div, tr, pre, etc.)
1472  // 4) Other block elements (p, h3, li, th, td, etc.)
1473  //
1474  // If we are inside a definition (1) list, we try to guess the users intent
1475  //   as to whether they want to insert* a new term or a new definition.
1476  // If we are inside any other list (2) element, we will insert* an li element.
1477  // If we are in any other container (3), we will insert* a p element.
1478  // If we are in any other block (4) element, we split the block into two
1479  //   pieces and move* anything after the cursor to the second block.
1480  //
1481  // *When inserting or moving content, we must be sure to look at any
1482  // inline elements that wrap the cursor, properly close them off, and create
1483  // the same group of wrapping inline elements in the inserted/moved
1484  // element. This logic is incorporated into splitTree.
1485
1486  // Find the first wrapping non-inline element. (1-5 above)
1487  if (ELEMENT_NODE == cursorParent.nodeType)
1488  {
1489      // When the cursor is on an element node, it's before that element in the
1490      // document, and so we only want to consider its parent for deciding what
1491      // to do. The same is true when the cursor points to just before a text
1492      // node, so we only need to check the cursorParent.
1493      var wrapNode = cursorParent;
1494  }
1495  else if (TEXT_NODE == cursorParent.nodeType)
1496  {
1497      // Since we know that a text node is not the wrapper, we'll start with
1498      // its parent.
1499      var wrapNode = cursorParent.parentNode;
1500  }
1501  else
1502  {
1503      // We are dealing with an XML document.  This should be expanded to
1504      // handle these cases.
1505      // http://www.w3schools.com/Dom/dom_nodetype.asp
1506      alert('You have selected a node from an XML document, type ' +
1507            cursorParent.nodeType + '.\nXML documents are not ' +
1508            'yet supported.');
1509      // Let the browser deal with it.
1510      return true;
1511  }
1512
1513  // This is an array used as a stack for recreating the current 'state' of
1514  // the cursor.  (eg. If the cursor is inside of an em tag inside of a p,
1515  // we'll add the em to the stack so that we can recreate it while splitting
1516  // the p.)
1517  var recreateStack = [];
1518
1519  while (!EnterParagraphs.prototype._pWrapper.test(wrapNode.nodeName))
1520  {
1521    recreateStack.push(wrapNode);
1522    wrapNode = wrapNode.parentNode;
1523
1524    if (!wrapNode)
1525    {
1526      // Broken DOM, let the browser handle it.
1527      return true;
1528    }
1529  }
1530
1531  if (wrapNode.nodeName.toLowerCase() in {pre:''})
1532  {
1533    insertLineBreak(cursorParent, cursorOffset, true, doc);
1534    this.editor.updateToolbar();
1535   
1536    Xinha._stopEvent(ev);
1537   
1538    range.setStart(cursorParent, cursorOffset+1);
1539    range.setEnd(cursorParent, cursorOffset+1);
1540    selection.removeAllRanges();
1541    selection.addRange(range);
1542    return false;
1543  }
1544  else if (wrapNode.nodeName.toLowerCase() in {body:'',div:'',fieldset:'',form:'',map:'',noscript:'','object':'',blockquote:''})
1545  {
1546    // We know that the there are no block elements between the cursor and the
1547    // wrapNode, but there may be inline elements.  What we'll do is take
1548    // everything in the tree below wrapNode, embed it into a P element, and
1549    // then split the whole thing.
1550
1551    // The cursor might be at the ending edge of the wrapNode.
1552    // 0. Pointing inside a completely empty element <body></body>
1553    // 1. Pointing to a text node <body>^This is text</body>
1554    // 2. Pointing to an inline node that is the child of the wrapNode.<body>^<em>text</em></body>
1555    // 3. Pointing to an inline node that is a non-direct descendant of the wrapNode.<body><q>^<em>text</em></q></body>
1556    // 4. Pointing to an inline node that is a non-direct descendant of the wrapNode.<body><q><em>text</em> this^</q></body>
1557    // 5. Pointing to the end of the wrapNode.<body>Here is some text.^</body>
1558    // 6. Pointing to a block node that is just inside of the wrapNode.<body>^<p>text</p></body>
1559
1560    // In the special case of a completely empty node, the cursor is still
1561    // visible, and the user expects to have two lines after hitting the enter
1562    // key.  We'll add two paragraphs to any of these nodes if they are empty.
1563    if (!wrapNode.firstChild) {
1564      var embedNode1 = doc.createElement('p');
1565      var embedNode2 = doc.createElement('p');
1566      embedNode1 = wrapNode.appendChild(embedNode1);
1567      embedNode2 = wrapNode.appendChild(embedNode2);
1568      // The unicode character below is a representation of a non-breaking
1569      // space we use to prevent the paragraph from having visual glitches.
1570      var emptyTextNode1 = doc.createTextNode('\u00a0');
1571      var emptyTextNode2 = doc.createTextNode('\u00a0');
1572      emptyTextNode1 = embedNode1.appendChild(emptyTextNode1);
1573      emptyTextNode2 = embedNode2.appendChild(emptyTextNode2);
1574
1575      Xinha._stopEvent(ev);
1576
1577      range.setStart(emptyTextNode2, 0);
1578      range.setEnd(emptyTextNode2, 0);
1579      selection.removeAllRanges();
1580      selection.addRange(range);
1581
1582      return false;
1583    }
1584
1585    var startNode = cursorParent;
1586    for (;(startNode != wrapNode) && (startNode.parentNode != wrapNode);)
1587    {
1588      startNode = startNode.parentNode;
1589    }
1590
1591    if (TEXT_NODE == cursorParent.nodeType)
1592    {
1593      var treeRoot = cursorParent;
1594    }
1595    else if (cursorOffset == cursorParent.childNodes.length)
1596    {
1597      var embedNode = doc.createElement('p');
1598      embedNode = wrapNode.appendChild(embedNode);
1599      // The unicode character below is a representation of a non-breaking
1600      // space we use to prevent the paragraph from having visual glitches.
1601      var emptyTextNode = doc.createTextNode('\u00a0');
1602      emptyTextNode = embedNode.appendChild(emptyTextNode);
1603
1604      Xinha._stopEvent(ev);
1605
1606      range.setStart(emptyTextNode, 0);
1607      range.setEnd(emptyTextNode, 0);
1608      selection.removeAllRanges();
1609      selection.addRange(range);
1610
1611      return false;
1612    }
1613    else
1614    {
1615      var treeRoot = cursorParent.childNodes[cursorOffset];
1616    }
1617
1618    for (;wrapNode != treeRoot.parentNode;)
1619    {
1620      treeRoot = treeRoot.parentNode;
1621    }
1622
1623    // At this point, treeRoot points to the root of the subtree inside
1624    // wrapNode that containes our cursor.  If this happens to be a block level
1625    // element, we'll just insert a P node here.  Otherwise, we'll replace this
1626    // node with an empty P node, and then embed it into that P node.
1627
1628    if (EnterParagraphs.prototype._pWrapper.test(treeRoot.nodeName))
1629    {
1630      var embedNode = doc.createElement('p');
1631      embedNode = wrapNode.insertBefore(embedNode, treeRoot);
1632      // The unicode character below is a representation of a non-breaking
1633      // space we use to prevent the paragraph from having visual glitches.
1634      var emptyTextNode = doc.createTextNode('\u00a0');
1635      emptyTextNode = embedNode.appendChild(emptyTextNode);
1636
1637      Xinha._stopEvent(ev);
1638
1639      range.setStart(treeRoot, 0);
1640      range.setEnd(treeRoot, 0);
1641      selection.removeAllRanges();
1642      selection.addRange(range);
1643
1644      return false;
1645    }
1646    var embedNode = doc.createElement('p');
1647
1648    treeRoot = wrapNode.replaceChild(embedNode, treeRoot);
1649
1650    treeRoot = embedNode.appendChild(treeRoot);
1651   
1652    if ((TEXT_NODE == treeRoot.nodeType) && !/\S/.test(treeRoot.nodeValue))
1653    {
1654      var newCursor = treeRoot;
1655    }
1656    else if (TEXT_NODE == treeRoot.nodeType)
1657    {
1658      var newCursor = splitTree(embedNode, treeRoot, cursorOffset, doc);
1659    }
1660    else
1661    {
1662      var parentOffset = this.indexInParent(treeRoot);
1663      if (null === parentOffset)
1664      {
1665        // We can't do anything with this cursor, so return.
1666        return;
1667      }
1668      var newCursor = splitTree(embedNode, treeRoot.parentNode, parentOffset, doc);
1669    }
1670  }
1671  else if (wrapNode.nodeName.toLowerCase() in {td:'',address:''})
1672  {
1673    // Line breaks BR element
1674    var newCursor = insertLineBreak(cursorParent, cursorOffset, false, doc);
1675  }
1676  else if (wrapNode.nodeName.toLowerCase() in {dl:''})
1677  {
1678    // Find the leftSibling of the cursorParent.  If none, insert dt (term) followed by dd (definition),
1679    // otherwise insert same as cursorParent followed by same as leftSibling.
1680    // Check to see if the leftSibling and rightSibling are the same and then just insert the one term.
1681    // XXX TODO
1682  }
1683  else if (wrapNode.nodeName.toLowerCase() in {h1:'',h2:'',h3:'',h4:'',h5:'',h6:'',p:''})
1684  {
1685    // Split wrapNode into two.
1686    var newCursor = splitTree(wrapNode, cursorParent, cursorOffset, doc);
1687  }
1688  else if (wrapNode.nodeName.toLowerCase() in {dt:'',dd:'',li:''})
1689  {
1690    // To the bane of software developers the world over, users expect to be
1691    // able to hit enter twice to end a list, whether at the end or in the
1692    // middle.  This means that we need to have special handling for list items
1693    // to check for the second return.  We do this by testing to see if the
1694    // current list item is empty, and if so, deleting it, splitting the list
1695    // into two if necessary, and inserting a paragraph.
1696    var newCursor = splitTree(wrapNode, cursorParent, cursorOffset, doc);
1697  }
1698  else if (wrapNode.nodeName.toLowerCase() in {ol:'',ul:''})
1699  {
1700    // Insert li
1701    var breaker = doc.createElement('li');
1702    if (TEXT_NODE == cursorParent.nodeType)
1703    {
1704      var newCursor = wrapNode.insertBefore(breaker,cursorParent);
1705    }
1706    else
1707    {
1708      var newCursor = wrapNode.insertBefore(breaker,cursorParent.childNodes[cursorOffset]);
1709    }
1710  }
1711
1712  this.editor.updateToolbar();
1713 
1714  Xinha._stopEvent(ev);
1715 
1716  // We turn the newCursor node into a cursor and offset into the parent.
1717  var newOffset = 0;
1718  while (newCursor.parentNode.childNodes[newOffset] != newCursor)
1719  {
1720    newOffset++;
1721  }
1722  newCursor = newCursor.parentNode;
1723
1724  // Monkey the new cursor position into somewhere the user should actually be
1725  // typing.
1726 
1727 
1728  Xinha._stopEvent(ev);
1729  range.setStart(newCursor, newOffset);
1730  range.setEnd(newCursor, newOffset);
1731  selection.removeAllRanges();
1732  selection.addRange(range);
1733  return false;
1734}
1735
1736/**
1737* If the cursor is on the edge of certain elements, we reposition it so that we
1738* can break the line in a way that's more useful to the user.
1739*/
1740
1741EnterParagraphs.prototype.moveCursorOnEdge = function(selection)
1742{
1743  // We'll only move the cursor if the selection is collapsed (ie. no contents)
1744  if ((selection.anchorNode != selection.focusNode) ||
1745      (selection.anchorOffset != selection.focusOffset))
1746  {
1747    return;
1748  }
1749
1750  // We now need to filter based on the element we are inside of.  If the
1751  // cursor is on a text node, we look at the parent of the node.
1752  var wrapNode = selection.anchorNode;
1753  if (TEXT_NODE == wrapNode.nodeType)
1754  {
1755    wrapNode = wrapNode.parentNode;
1756  }
1757 
1758  // Check the wrapper against our lists of trigger nodes.
1759  if (!EnterParagraphs.prototype._pifyParent.test(wrapNode.nodeName) &&
1760      !EnterParagraphs.prototype._pifySibling.test(wrapNode.nodeName))
1761  {
1762    // We're lucky, no need to check for edges, let's just return.
1763    return;
1764  }
1765
1766  // Okay, time to perform edge checking.  If the cursor is inside of a text
1767  // node, the rules for edge detection are quite specialized, so we'll deal
1768  // with that first.  Since text nodes can't contain other nodes, we only have
1769  // to perform this check once.  We won't actually move the cursor here, just
1770  // our copy of it, because we won't know where it belongs until we're dealing
1771  // with the nodes themselves, rather than the text.
1772
1773  var cursorParent = selection.anchorNode;
1774  var cursorOffset = selection.anchorOffset;
1775
1776  for (;this.cursorAtEnd(cursorParent, cursorOffset);)
1777  {
1778    if (TEXT_NODE == cursorParent.nodeType)
1779    {
1780      // If we're at the end and stuck inside of a text node, we move out of
1781      // the text node, which is a simpler case, than continue.
1782      var parentOffset = this.indexInParent(cursorParent);
1783      if (null === parentOffset)
1784      {
1785        // We can't do anything with this cursor, so return.
1786        return;
1787      }
1788
1789      cursorParent = cursorParent.parentNode;
1790      cursorOffset = parentOffset + 1;
1791      continue;
1792    }
1793
1794    var parentOffset = this.indexInParent(cursorParent);
1795    if (null === parentOffset)
1796    {
1797      // We can't do anything with this cursor, so return.
1798      return;
1799    }
1800
1801    cursorParent = cursorParent.parentNode;
1802    cursorOffset = parentOffset + 1;
1803
1804    // If we are no longer inside of one of our trigger nodes, we're done.
1805    if (!this._pifyParent.test(cursorParent.nodeName) &&
1806        !this._pifySibling.test(cursorParent.nodeName))
1807    {
1808      // Move the real cursor.
1809      selection.removeAllRanges();
1810      var range = this.editor.createRange(selection);
1811      range.setStart(cursorParent, cursorOffset);
1812      range.setEnd(cursorParent, cursorOffset);
1813      selection.addRange(range);
1814      return;
1815    }
1816  }
1817
1818  for (;this.cursorAtBeginning(cursorParent, cursorOffset);)
1819  {
1820    if (TEXT_NODE == cursorParent.nodeType)
1821    {
1822      // If we're at the beginning and stuck inside of a text node, we move out
1823      // of the text node, which is a simpler case, than continue.
1824      var parentOffset = this.indexInParent(cursorParent);
1825      if (null === parentOffset)
1826      {
1827        // We can't do anything with this cursor, so return.
1828        return;
1829      }
1830
1831      cursorParent = cursorParent.parentNode;
1832      cursorOffset = parentOffset;
1833      continue;
1834    }
1835
1836    var parentOffset = this.indexInParent(cursorParent);
1837    if (null === parentOffset)
1838    {
1839      // We can't do anything with this cursor, so return.
1840      return;
1841    }
1842
1843    cursorParent = cursorParent.parentNode;
1844    cursorOffset = parentOffset;
1845
1846    // If we are no longer inside of one of our trigger nodes, we're done.
1847    if (!this._pifyParent.test(cursorParent.nodeName) &&
1848        !this._pifySibling.test(cursorParent.nodeName))
1849    {
1850      // Move the real cursor.
1851      selection.removeAllRanges();
1852      var range = this.editor.createRange(selection);
1853      range.setStart(cursorParent, cursorOffset);
1854      range.setEnd(cursorParent, cursorOffset);
1855      selection.addRange(range);
1856      return;
1857    }
1858  }
1859}
1860
1861EnterParagraphs.prototype.handleEnter = function(ev)
1862{
1863 
1864  var cursorNode;
1865 
1866  // Grab the selection and associated range
1867 
1868  var sel = this.editor.getSelection();
1869  var rng = this.editor.createRange(sel);
1870 
1871  // if we are at the end of a list and the node is empty let the browser handle
1872  // it to get us out of the list.
1873 
1874  if ( this.isNormalListItem(rng) )
1875  {
1876    return true;
1877  }
1878 
1879  // as far as I can tell this isn't actually used.
1880 
1881  this.takenIds = new Object();
1882 
1883  // Grab ranges for document re-stuffing, if appropriate
1884  //
1885  // pStart and pEnd are arrays consisting of
1886  // [0] neighbor node
1887  // [1] insertion type
1888  // [2] roam
1889 
1890  var pStart = this.processSide(rng, "left");
1891 
1892  var pEnd = this.processSide(rng, "right");
1893 
1894  // used to position the cursor after insertion.
1895 
1896  cursorNode = pEnd[2];
1897 
1898  // Get rid of everything local to the selection
1899 
1900  sel.removeAllRanges();
1901  rng.deleteContents();
1902 
1903  // Grab a node we'll have after insertion, since fragments will be lost
1904  //
1905  // we'll use this to position the cursor.
1906 
1907  var holdEnd = this.forEachNodeUnder( cursorNode, "find_cursorpoint", "ltr", false, true);
1908 
1909  if ( ! holdEnd )
1910  {
1911    alert( "INTERNAL ERROR - could not find place to put cursor after ENTER" );
1912  }
1913 
1914  // Insert our carefully chosen document fragments
1915 
1916  if ( pStart )
1917  {
1918   
1919    this.insertAdjacentElement(pStart[0], pStart[1], pStart[2]);
1920  }
1921 
1922  if ( pEnd && pEnd.nodeType != 1)
1923  {
1924   
1925    this.insertAdjacentElement(pEnd[0], pEnd[1], pEnd[2]);
1926  }
1927 
1928  // Move the caret in front of the first good text element
1929 
1930  if ((holdEnd) && (this._permEmpty.test(holdEnd.nodeName) ))
1931  {
1932   
1933    var prodigal = 0;
1934    while ( holdEnd.parentNode.childNodes.item(prodigal) != holdEnd )
1935    {
1936      prodigal++;
1937    }
1938   
1939    sel.collapse( holdEnd.parentNode, prodigal);
1940  }
1941  else
1942  {
1943   
1944    // holdEnd might be false.
1945   
1946    try
1947    {
1948      sel.collapse(holdEnd, 0);
1949     
1950      // interestingly, scrollToElement() scroll so the top if holdEnd is a text node.
1951     
1952      if ( holdEnd.nodeType == 3 )
1953      {
1954        holdEnd = holdEnd.parentNode;
1955      }
1956     
1957      this.editor.scrollToElement(holdEnd);
1958    }
1959    catch (e)
1960    {
1961      // we could try to place the cursor at the end of the document.
1962    }
1963  }
1964 
1965  this.editor.updateToolbar();
1966 
1967  Xinha._stopEvent(ev);
1968 
1969  return true;
1970 
1971};      // end of handleEnter()
1972
1973// END
Note: See TracBrowser for help on using the repository browser.