// tabs 2 /** * @fileoverview By Adam Wright, for The University of Western Australia * * Distributed under the same terms as HTMLArea itself. * This notice MUST stay intact for use (see license.txt). * * Heavily modified by Yermo Lamers of DTLink, LLC, College Park, Md., USA. * For more info see http://www.areaedit.com */ /** * plugin Info */ EnterParagraphs._pluginInfo = { name : "EnterParagraphs", version : "1.0", developer : "Adam Wright", developer_url : "http://www.hipikat.org/", sponsor : "The University of Western Australia", sponsor_url : "http://www.uwa.edu.au/", license : "htmlArea" }; // ------------------------------------------------------------------ // "constants" /** * Whitespace Regex */ EnterParagraphs.prototype._whiteSpace = /^\s*$/; /** * The pragmatic list of which elements a paragraph may not contain */ EnterParagraphs.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; /** * elements which may contain a paragraph */ EnterParagraphs.prototype._pContainers = /^(body|del|div|fieldset|form|ins|map|noscript|object|td|th)$/i; /** * Elements which may not contain paragraphs, and would prefer a break to being split */ EnterParagraphs.prototype._pBreak = /^(address|pre|blockquote)$/i; /** * Elements which may not contain children */ EnterParagraphs.prototype._permEmpty = /^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i; /** * Elements which count as content, as distinct from whitespace or containers */ EnterParagraphs.prototype._elemSolid = /^(applet|br|button|hr|img|input|table)$/i; /** * Elements which should get a new P, before or after, when enter is pressed at either end */ EnterParagraphs.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; EnterParagraphs.prototype._pifyForced = /^(ul|ol|dl|table)$/i; /** * Elements which should get a new P, before or after a close parent, when enter is pressed at either end */ EnterParagraphs.prototype._pifyParent = /^(dd|dt|li|td|th|tr)$/i; // --------------------------------------------------------------------- /** * EnterParagraphs Constructor */ function EnterParagraphs(editor) { this.editor = editor; // [STRIP // create a ddt debug trace object. There may be multiple editors on // the page each EnterParagraphs .. to distinguish which instance // is generating the message we tack on the name of the textarea. //this.ddt = new DDT( editor._textArea + ":EnterParagraphs Plugin" ); // uncomment to turn on debugging messages. //this.ddt._ddtOn(); //this.ddt._ddt( "enter-paragraphs.js","23", "EnterParagraphs(): constructor" ); // STRIP] // hook into the event handler to intercept key presses if we are using // gecko (Mozilla/FireFox) if (HTMLArea.is_gecko) { //this.ddt._ddt( "enter-paragraphs.js","23", "EnterParagraphs(): we are gecko. Setting event handler." ); this.onKeyPress = this.__onKeyPress; } } // end of constructor. // ------------------------------------------------------------------ /** * name member for debugging * * This member is used to identify objects of this class in debugging * messages. */ EnterParagraphs.prototype.name = "EnterParagraphs"; /** * Gecko's a bit lacking in some odd ways... */ EnterParagraphs.prototype.insertAdjacentElement = function(ref,pos,el) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "122", "insertAdjacentElement(): top with pos '" + pos + "' ref:", ref ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "122", "insertAdjacentElement(): top with el:", el ); if ( pos == 'BeforeBegin' ) { ref.parentNode.insertBefore(el,ref); } else if ( pos == 'AfterEnd' ) { ref.nextSibling ? ref.parentNode.insertBefore(el,ref.nextSibling) : ref.parentNode.appendChild(el); } else if ( pos == 'AfterBegin' && ref.firstChild ) { ref.insertBefore(el,ref.firstChild); } else if ( pos == 'BeforeEnd' || pos == 'AfterBegin' ) { ref.appendChild(el); } //this.ddt._ddtDumpNode( "enter-paragraphs.js", "122", "insertAdjacentElement(): bottom with ref:", ref ); }; // end of insertAdjacentElement() // ---------------------------------------------------------------- /** * Passes a global parent node or document fragment to forEachNode * * @param root node root node to start search from. * @param mode string function to apply to each node. * @param direction string traversal direction "ltr" (left to right) or "rtl" (right_to_left) * @param init boolean */ EnterParagraphs.prototype.forEachNodeUnder = function ( root, mode, direction, init ) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "144", "forEachNodeUnder(): top mode is '" + mode + "' direction is '" + direction + "' starting with root node:", root ); // Identify the first and last nodes to deal with var start, end; // nodeType 11 is DOCUMENT_FRAGMENT_NODE which is a container. if ( root.nodeType == 11 && root.firstChild ) { start = root.firstChild; end = root.lastChild; } else { start = end = root; } //this.ddt._ddtDumpNode( "enter-paragraphs.js", "144", "forEachNodeUnder(): start node is:", start ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "144", "forEachNodeUnder(): initial end node is:", end ); // traverse down the right hand side of the tree getting the last child of the last // child in each level until we reach bottom. while ( end.lastChild ) end = end.lastChild; //this.ddt._ddtDumpNode( "enter-paragraphs.js", "144", "forEachNodeUnder(): end node after descent is:", end ); return this.forEachNode( start, end, mode, direction, init); }; // end of forEachNodeUnder() // ----------------------------------------------------------------------- /** * perform a depth first descent in the direction requested. * * @param left_node node "start node" * @param right_node node "end node" * @param mode string function to apply to each node. cullids or emptyset. * @param direction string traversal direction "ltr" (left to right) or "rtl" (right_to_left) * @param init boolean or object. */ EnterParagraphs.prototype.forEachNode = function (left_node, right_node, mode, direction, init) { //this.ddt._ddt( "enter-paragraphs.js", "175", "forEachNode(): top - mode is:" + mode + "' direction '" + direction + "'" ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "175", "forEachNode(): top - left node is:", left_node ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "175", "forEachNode(): top - right node is:", right_node ); // returns "Brother" node either left or right. var getSibling = function(elem, direction) { return ( direction == "ltr" ? elem.nextSibling : elem.previousSibling ); }; var getChild = function(elem, direction) { return ( direction == "ltr" ? elem.firstChild : elem.lastChild ); }; var walk, lookup, fnReturnVal; // FIXME: init is a boolean in the emptyset case and an object in // the cullids case. Used inconsistently. var next_node = init; // used to flag having reached the last node. var done_flag = false; // loop ntil we've hit the last node in the given direction. // if we're going left to right that's the right_node and visa-versa. while ( walk != direction == "ltr" ? right_node : left_node ) { // on first entry, walk here is null. So this is how // we prime the loop with the first node. if ( !walk ) { walk = direction == "ltr" ? left_node : right_node; //this.ddt._ddtDumpNode( "enter-paragraphs.js", "175", "forEachNode(): !walk - current node is:", walk ); } else { // is there a child node? if ( getChild(walk,direction) ) { // descend down into the child. walk = getChild(walk,direction); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "175", "forEachNode():descending to child node:", walk ); } else { // is there a sibling node on this level? if ( getSibling(walk,direction) ) { // move to the sibling. walk = getSibling(walk,direction); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "175", "forEachNode(): moving to sibling node:", walk ); } else { lookup = walk; // climb back up the tree until we find a level where we are not the end // node on the level (i.e. that we have a sibling in the direction // we are searching) or until we reach the end. while ( !getSibling(lookup,direction) && lookup != (direction == "ltr" ? right_node : left_node) ) { lookup = lookup.parentNode; } // did we find a level with a sibling? // walk = ( lookup.nextSibling ? lookup.nextSibling : lookup ) ; walk = ( getSibling(lookup,direction) ? getSibling(lookup,direction) : lookup ) ; //this.ddt._ddtDumpNode( "enter-paragraphs.js", "175", "forEachNode(): climbed back up (or found right node):", walk ); } } } // end of else walk. // have we reached the end? either as a result of the top while loop or climbing // back out above. done_flag = (walk==( direction == "ltr" ? right_node : left_node)); // call the requested function on the current node. Functions // return an array. // // Possible functions are _fenCullIds, _fenEmptySet // // The situation is complicated by the fact that sometimes we want to // return the base node and sometimes we do not. // // next_node can be an object (this.takenIds), a node (text, el, etc) or false. //this.ddt._ddt( "enter-paragraphs.js", "175", "forEachNode(): calling function" ); switch( mode ) { case "cullids": fnReturnVal = this._fenCullIds(walk, next_node ); break; case "find_fill": fnReturnVal = this._fenEmptySet(walk, next_node, mode, done_flag); break; case "find_cursorpoint": fnReturnVal = this._fenEmptySet(walk, next_node, mode, done_flag); break; } // If this node wants us to return, return next_node if ( fnReturnVal[0] ) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "175", "forEachNode(): returning node:", fnReturnVal[1] ); return fnReturnVal[1]; } // are we done with the loop? if ( done_flag ) { break; } // Otherwise, pass to the next node if ( fnReturnVal[1] ) { next_node = fnReturnVal[1]; } } // end of while loop //this.ddt._ddt( "enter-paragraphs.js", "175", "forEachNode(): returning false." ); return false; }; // end of forEachNode() // ------------------------------------------------------------------- /** * Find a post-insertion node, only if all nodes are empty, or the first content * * @param node node current node beinge examined. * @param next_node node next node to be examined. * @param node string "find_fill" or "find_cursorpoint" * @param last_flag boolean is this the last node? */ EnterParagraphs.prototype._fenEmptySet = function( node, next_node, mode, last_flag) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "263", "_fenEmptySet() : top with mode '" + mode + "' and last_flag '" + last_flag + "' and node:", node ); // Mark this if it's the first base if ( !next_node && !node.firstChild ) { next_node = node; } // Is it an element node and is it considered content? (br, hr, etc) // or is it a text node that is not just whitespace? // or is it not an element node and not a text node? if ( (node.nodeType == 1 && this._elemSolid.test(node.nodeName)) || (node.nodeType == 3 && !this._whiteSpace.test(node.nodeValue)) || (node.nodeType != 1 && node.nodeType != 3) ) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "263", "_fenEmptySet() : found content in node:", node ); switch( mode ) { case "find_fill": // does not return content. return new Array(true, false ); breal; case "find_cursorpoint": // returns content return new Array(true, node ); break; } } // In either case (fill or findcursor) we return the base node. The avoids // problems in terminal cases (beginning or end of document or container tags) if ( last_flag ) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "263", "_fenEmptySet() : return 'base' node:", next_node ); return new Array( true, next_node ); } //this.ddt._ddtDumpNode( "enter-paragraphs.js", "263", "_fenEmptySet() : bottom returning false and :", next_node ); return new Array( false, next_node ); }; // end of _fenEmptySet() // ------------------------------------------------------------------------------ /** * remove duplicate Id's. * * @param ep_ref enterparagraphs reference to enterparagraphs object */ EnterParagraphs.prototype._fenCullIds = function ( ep_ref, node, pong ) { //this.ddt._ddt( "enter-paragraphs.js", "299", "_fenCullIds(): top" ); // Check for an id, blast it if it's in the store, otherwise add it if ( node.id ) { //this.ddt._ddt( "enter-paragraphs.js", "299", "_fenCullIds(): node '" + node.nodeName + "' has an id '" + node.id + "'" ); pong[node.id] ? node.id = '' : pong[node.id] = true; } return new Array(false,pong); }; // --------------------------------------------------------------------------------- /** * Grabs a range suitable for paragraph stuffing * * @param rng Range * @param search_direction string "left" or "right" * * @todo check blank node issue in roaming loop. */ EnterParagraphs.prototype.processSide = function( rng, search_direction) { //this.ddt._ddt( "enter-paragraphs.js", "329", "processSide(): top search_direction == '" + search_direction + "'" ); var next = function(element, search_direction) { return ( search_direction == "left" ? element.previousSibling : element.nextSibling ); }; var node = search_direction == "left" ? rng.startContainer : rng.endContainer; var offset = search_direction == "left" ? rng.startOffset : rng.endOffset; var roam, start = node; //this.ddt._ddtDumpNode( "enter-paragraphs.js", "337", "processSide(): starting with node:", node ); // Never start with an element, because then the first roaming node might // be on the exclusion list and we wouldn't know until it was too late while ( start.nodeType == 1 && !this._permEmpty.test(start.nodeName) ) { start = ( offset ? start.lastChild : start.firstChild ); } // Climb the tree, left or right, until our course of action presents itself // // if roam is NULL try start. // if roam is NOT NULL, try next node in our search_direction // If that node is NULL, get our parent node. // // If all the above turns out NULL end the loop. // // FIXME: gecko (firefox 1.0.3) - enter "test" into an empty document and press enter. // sometimes this loop finds a blank text node, sometimes it doesn't. while ( roam = roam ? ( next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode ) : start ) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "357", "processSide(): roaming loop, search_direction is '" + search_direction + "' current node is: ", roam ); // next() is an inline function defined above that returns the next node depending // on the direction we're searching. if ( next(roam,search_direction) ) { //this.ddt._ddt( "enter-paragraphs.js", "371", "processSide(): Checking next node '" + next(roam,search_direction).NodeName + "' for _pExclusions list." ); // If the next sibling's on the exclusion list, stop before it if ( this._pExclusions.test(next(roam,search_direction).nodeName) ) { //this.ddt._ddt( "enter-paragraphs.js", "371", "processSide(): Node '" + next(roam,search_direction).NodeName + "' is on the _pExclusions list. Stopping before it." ); return this.processRng(rng, search_direction, roam, next(roam,search_direction), (search_direction == "left"?'AfterEnd':'BeforeBegin'), true, false); } } else { //this.ddt._ddt( "enter-paragraphs.js", "371", "processSide(): No next node, examing parent node '" + roam.parentNode.nodeName + "' for containers or exclusions." ); // If our parent's on the container list, stop inside it if (this._pContainers.test(roam.parentNode.nodeName)) { //this.ddt._ddt( "enter-paragraphs.js", "371", "processSide(): Parent Node '" + roam.parentNode.nodeName + "' is on the _pContainer list. Stopping inside it." ); return this.processRng(rng, search_direction, roam, roam.parentNode, (search_direction == "left"?'AfterBegin':'BeforeEnd'), true, false); } else if (this._pExclusions.test(roam.parentNode.nodeName)) { //this.ddt._ddt( "enter-paragraphs.js", "371", "processSide(): Parent Node '" + roam.parentNode.nodeName + "' is on the _pExclusion list." ); // chop without wrapping if (this._pBreak.test(roam.parentNode.nodeName)) { //this.ddt._ddt( "enter-paragraphs.js", "371", "processSide(): Parent Node '" + roam.parentNode.nodeName + "' is on the _pBreak list." ); return this.processRng(rng, search_direction, roam, roam.parentNode, (search_direction == "left"?'AfterBegin':'BeforeEnd'), false, (search_direction == "left" ?true:false)); } else { //this.ddt._ddt( "enter-paragraphs.js", "371", "processSide(): Parent Node '" + roam.parentNode.nodeName + "' is not on the _pBreak list." ); // the next(roam,search_direction) in this call is redundant since we know it's false // because of the "if next(roam,search_direction)" above. // // the final false prevents this range from being wrapped in
's most likely // because it's already wrapped. return this.processRng(rng, search_direction, (roam = roam.parentNode), (next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode), (next(roam,search_direction) ? (search_direction == "left"?'AfterEnd':'BeforeBegin') : (search_direction == "left"?'AfterBegin':'BeforeEnd')), false, false); } } } } //this.ddt._ddt( "enter-paragraphs.js", "424", "processSide(): bottom" ); }; // end of processSide() // ------------------------------------------------------------------------------ /** * processRng - process Range. * * Neighbour and insertion identify where the new node, roam, needs to enter * the document; landmarks in our selection will be deleted before insertion * * @param rn Range original selected range * @param search_direction string Direction to search in. * @param roam node * @param insertion string may be AfterBegin of BeforeEnd * @return array */ EnterParagraphs.prototype.processRng = function(rng, search_direction, roam, neighbour, insertion, pWrap, preBr) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "398", "processRng(): top - roam arg is:", roam ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "398", "processRng(): top - neighbor arg is:", neighbour ); //this.ddt._ddt( "enter-paragraphs.js", "398", "processRng(): top - insertion arg is: '" + insertion + "'" ); var node = search_direction == "left" ? rng.startContainer : rng.endContainer; var offset = search_direction == "left" ? rng.startOffset : rng.endOffset; //this.ddt._ddtDumpNode( "enter-paragraphs.js", "447", "processRng(): range start (or end) is at offset '" + offset + "' is node :", node ); // Define the range to cut, and extend the selection range to the same boundary var editor = this.editor; var newRng = editor._doc.createRange(); newRng.selectNode(roam); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "522", "processRng(): selecting newRng is:", newRng ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "522", "processRng(): selecting original rng is:", rng ); // extend the range in the given direction. if ( search_direction == "left") { newRng.setEnd(node, offset); rng.setStart(newRng.startContainer, newRng.startOffset); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "522", "processRng(): extending direction left - newRng is:", newRng ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "522", "processRng(): extending direction left - rng is:", rng ); } else if ( search_direction == "right" ) { newRng.setStart(node, offset); rng.setEnd(newRng.endContainer, newRng.endOffset); //this.ddt._ddt( "enter-paragraphs.js", "522", "processRng(): right - new range start is '" + offset + "' end offset is '" + newRng.endOffset + "'" ); } //this.ddt._ddtDumpNode( "enter-paragraphs.js", "522", "processRng(): rng is:", rng ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "522", "processRng(): newRng is:", newRng ); // Clone the range and remove duplicate ids it would otherwise produce var cnt = newRng.cloneContents(); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "509", "processRng(): culling duplicate ids from:", cnt ); // in this case "init" is an object not a boolen. this.forEachNodeUnder( cnt, "cullids", "ltr", this.takenIds, false, false); // Special case, for inserting paragraphs before some blocks when caret is at // their zero offset. // // Used to "open up space" in front of a list, table. Usefull if the list is at // the top of the document. (otherwise you'd have no way of "moving it down"). var pify, pifyOffset, fill; pify = search_direction == "left" ? (newRng.endContainer.nodeType == 3 ? true:false) : (newRng.startContainer.nodeType == 3 ? false:true); pifyOffset = pify ? newRng.startOffset : newRng.endOffset; pify = pify ? newRng.startContainer : newRng.endContainer; //this.ddt._ddtDumpNode( "enter-paragraphs.js", "521", "processRng(): pify is '" + pify.nodeName + "' pifyOffset is '" + pifyOffset + "':", pify ); if ( this._pifyParent.test(pify.nodeName) && pify.parentNode.childNodes.item(0) == pify ) { while ( !this._pifySibling.test(pify.nodeName) ) { pify = pify.parentNode; } } // NODE TYPE 11 is DOCUMENT_FRAGMENT NODE if ( cnt.nodeType == 11 && !cnt.firstChild ) { cnt.appendChild(editor._doc.createElement(pify.nodeName)); } // YmL: Added additional last parameter for fill case to work around logic // error in forEachNode() //this.ddt._ddt( "enter-paragraphs.js", "612", "processRng(): find_fill in cnt." ); fill = this.forEachNodeUnder(cnt, "find_fill", "ltr", false ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "612", "processRng(): fill node:" , fill ); if ( fill && this._pifySibling.test(pify.nodeName) && ( (pifyOffset == 0) || ( pifyOffset == 1 && this._pifyForced.test(pify.nodeName) ) ) ) { //this.ddt._ddt( "enter-paragraphs.js", "544", "processRng(): pify handling. Creating p tag followed by nbsp tag" ); roam = editor._doc.createElement( 'p' ); roam.innerHTML = " "; // roam = editor._doc.createElement('p'); // roam.appendChild(editor._doc.createElement('br')); // for these cases, if we are processing the left hand side we want it to halt // processing instead of doing the right hand side. (Avoids adding another
 
// after the list etc. if ((search_direction == "left" ) && pify.previousSibling) { //this.ddt._ddt( "enter-paragraphs.js", "682", "processRng(): returning created roam AfterEnd" ); return new Array(pify.previousSibling, 'AfterEnd', roam); } else if (( search_direction == "right") && pify.nextSibling) { //this.ddt._ddt( "enter-paragraphs.js", "682", "processRng(): returning created roam BeforeBegin" ); return new Array(pify.nextSibling, 'BeforeBegin', roam); } else { //this.ddt._ddt( "enter-paragraphs.js", "682", "processRng(): returning created roam for direction '" + search_direction + "'" ); return new Array(pify.parentNode, (search_direction == "left"?'AfterBegin':'BeforeEnd'), roam); } } // If our cloned contents are 'content'-less, shove a break in them if ( fill ) { // Ill-concieved? // // 3 is a TEXT node and it should be empty. // if ( fill.nodeType == 3 ) { // fill = fill.parentNode; fill = editor._doc.createDocumentFragment(); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "575", "processRng(): fill.nodeType is 3. Moving up to parent:", fill ); } if ( (fill.nodeType == 1 && !this._elemSolid.test()) || fill.nodeType == 11 ) { // FIXME:/CHECKME: When Xinha is switched from WYSIWYG to text mode // HTMLArea.getHTMLWrapper() will strip out the trailing br. Not sure why. // fill.appendChild(editor._doc.createElement('br')); var pterminator = editor._doc.createElement( 'p' ); pterminator.innerHTML = " "; fill.appendChild( pterminator ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "583", "processRng(): fill type is 1 and !elemsolid or it's type 11. Appending an nbsp tag:", fill ); } else { //this.ddt._ddt( "enter-paragraphs.js", "583", "processRng(): inserting a br tag before." ); // fill.parentNode.insertBefore(editor._doc.createElement('br'),fill); var pterminator = editor._doc.createElement( 'p' ); pterminator.innerHTML = " "; fill.parentNode.insertBefore(parentNode,fill); } } // YmL: If there was no content replace with fill // (previous code did not use fill and we ended up with the //test
because Gecko was finding two empty text nodes // when traversing on the right hand side of an empty document. if ( fill ) { //this.ddt._ddtDumpNode( "enter-paragraphs.js", "606", "processRng(): no content. Using fill.", fill ); roam = fill; } else { // And stuff a shiny new object with whatever contents we have //this.ddt._ddt( "enter-paragraphs.js", "606", "processRng(): creating p tag or document fragment - pWrap is '" + pWrap + "' " ); roam = (pWrap || (cnt.nodeType == 11 && !cnt.firstChild)) ? editor._doc.createElement('p') : editor._doc.createDocumentFragment(); roam.appendChild(cnt); } if (preBr) { //this.ddt._ddt( "enter-paragraphs.js", "767", "processRng(): appending a br based on preBr flag" ); roam.appendChild(editor._doc.createElement('br')); } //this.ddt._ddtDumpNode( "enter-paragraphs.js", "606", "processRng(): bottom with roam:", roam ); //this.ddt._ddtDumpNode( "enter-paragraphs.js", "606", "processRng(): bottom with neighbour:", neighbour ); // Return the nearest relative, relative insertion point and fragment to insert return new Array(neighbour, insertion, roam); }; // end of processRng() // ---------------------------------------------------------------------------------- /** * are we an 
inserted before the list. The * built-in behavior is to open up a