blob: 318c519fdbe6f8f70738a901b9b3b9cd3f49287c [file] [log] [blame]
// tabs 2
/**
* @fileoverview By Adam Wright, for The University of Western Australia
*
* Distributed under the same terms as Xinha 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;
// hook into the event handler to intercept key presses if we are using
// gecko (Mozilla/FireFox)
if (Xinha.is_gecko)
{
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)
{
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);
}
}; // 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 )
{
// 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;
}
// 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;
}
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)
{
// 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;
}
else
{
// is there a child node?
if ( getChild(walk,direction) )
{
// descend down into the child.
walk = getChild(walk,direction);
}
else
{
// is there a sibling node on this level?
if ( getSibling(walk,direction) )
{
// move to the sibling.
walk = getSibling(walk,direction);
}
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 ) ;
}
}
} // 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.
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] )
{
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
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)
{
// 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) )
{
switch( mode )
{
case "find_fill":
// does not return content.
return new Array(true, false );
break;
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 )
{
return new Array( true, 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 )
{
// Check for an id, blast it if it's in the store, otherwise add it
if ( 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)
{
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;
// 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 )
{
// next() is an inline function defined above that returns the next node depending
// on the direction we're searching.
if ( next(roam,search_direction) )
{
// If the next sibling's on the exclusion list, stop before it
if ( this._pExclusions.test(next(roam,search_direction).nodeName) )
{
return this.processRng(rng, search_direction, roam, next(roam,search_direction), (search_direction == "left"?'AfterEnd':'BeforeBegin'), true, false);
}
}
else
{
// If our parent's on the container list, stop inside it
if (this._pContainers.test(roam.parentNode.nodeName))
{
return this.processRng(rng, search_direction, roam, roam.parentNode, (search_direction == "left"?'AfterBegin':'BeforeEnd'), true, false);
}
else if (this._pExclusions.test(roam.parentNode.nodeName))
{
// chop without wrapping
if (this._pBreak.test(roam.parentNode.nodeName))
{
return this.processRng(rng, search_direction, roam, roam.parentNode,
(search_direction == "left"?'AfterBegin':'BeforeEnd'), false, (search_direction == "left" ?true:false));
}
else
{
// 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 <p>'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);
}
}
}
}
}; // 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)
{
var node = search_direction == "left" ? rng.startContainer : rng.endContainer;
var offset = search_direction == "left" ? rng.startOffset : rng.endOffset;
// 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);
// extend the range in the given direction.
if ( search_direction == "left")
{
newRng.setEnd(node, offset);
rng.setStart(newRng.startContainer, newRng.startOffset);
}
else if ( search_direction == "right" )
{
newRng.setStart(node, offset);
rng.setEnd(newRng.endContainer, newRng.endOffset);
}
// Clone the range and remove duplicate ids it would otherwise produce
var cnt = newRng.cloneContents();
// 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;
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
// I do not profess to understand any of this, simply applying a patch that others say is good - ticket:446
if ( cnt.nodeType == 11 && !cnt.firstChild)
{
if (pify.nodeName != "BODY" || (pify.nodeName == "BODY" && pifyOffset != 0))
{ //WKR: prevent body tag in empty doc
cnt.appendChild(editor._doc.createElement(pify.nodeName));
}
}
// YmL: Added additional last parameter for fill case to work around logic
// error in forEachNode()
fill = this.forEachNodeUnder(cnt, "find_fill", "ltr", false );
if ( fill &&
this._pifySibling.test(pify.nodeName) &&
( (pifyOffset == 0) || ( pifyOffset == 1 && this._pifyForced.test(pify.nodeName) ) ) )
{
roam = editor._doc.createElement( 'p' );
roam.innerHTML = "&nbsp;";
// 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 <p>&nbsp</p>
// after the list etc.
if ((search_direction == "left" ) && pify.previousSibling)
{
return new Array(pify.previousSibling, 'AfterEnd', roam);
}
else if (( search_direction == "right") && pify.nextSibling)
{
return new Array(pify.nextSibling, 'BeforeBegin', roam);
}
else
{
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();
}
if ( (fill.nodeType == 1 && !this._elemSolid.test()) || fill.nodeType == 11 )
{
// FIXME:/CHECKME: When Xinha is switched from WYSIWYG to text mode
// Xinha.getHTMLWrapper() will strip out the trailing br. Not sure why.
// fill.appendChild(editor._doc.createElement('br'));
var pterminator = editor._doc.createElement( 'p' );
pterminator.innerHTML = "&nbsp;";
fill.appendChild( pterminator );
}
else
{
// fill.parentNode.insertBefore(editor._doc.createElement('br'),fill);
var pterminator = editor._doc.createElement( 'p' );
pterminator.innerHTML = "&nbsp;";
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
// <p>test</p><p></p> because Gecko was finding two empty text nodes
// when traversing on the right hand side of an empty document.
if ( fill )
{
roam = fill;
}
else
{
// And stuff a shiny new object with whatever contents we have
roam = (pWrap || (cnt.nodeType == 11 && !cnt.firstChild)) ? editor._doc.createElement('p') : editor._doc.createDocumentFragment();
roam.appendChild(cnt);
}
if (preBr)
{
roam.appendChild(editor._doc.createElement('br'));
}
// Return the nearest relative, relative insertion point and fragment to insert
return new Array(neighbour, insertion, roam);
}; // end of processRng()
// ----------------------------------------------------------------------------------
/**
* are we an <li> that should be handled by the browser?
*
* there is no good way to "get out of" ordered or unordered lists from Javascript.
* We have to pass the onKeyPress 13 event to the browser so it can take care of
* getting us "out of" the list.
*
* The Gecko engine does a good job of handling all the normal <li> cases except the "press
* enter at the first position" where we want a <p>&nbsp</p> inserted before the list. The
* built-in behavior is to open up a <li> before the current entry (not good).
*
* @param rng Range range.
*/
EnterParagraphs.prototype.isNormalListItem = function(rng)
{
var node, listNode;
node = rng.startContainer;
if (( typeof node.nodeName != 'undefined') &&
( node.nodeName.toLowerCase() == 'li' ))
{
// are we a list item?
listNode = node;
}
else if (( typeof node.parentNode != 'undefined' ) &&
( typeof node.parentNode.nodeName != 'undefined' ) &&
( node.parentNode.nodeName.toLowerCase() == 'li' ))
{
// our parent is a list item.
listNode = node.parentNode;
}
else
{
// neither we nor our parent are a list item. this is not a normal
// li case.
var listNode = this.editor._getFirstAncestorForNodeAndWhy(node, ["li"])[0];
if ( typeof listNode == 'undefined' || !listNode )
{
return false;
}
}
// at this point we have a listNode. Is it the first list item?
if ( ! listNode.previousSibling )
{
// are we on the first character of the first li?
if ( rng.startOffset == 0 )
{
return false;
}
}
return true;
}; // end of isNormalListItem()
// ----------------------------------------------------------------------------------
/**
* Called when a key is pressed in the editor
*/
EnterParagraphs.prototype.__onKeyPress = function(ev)
{
// If they've hit enter and shift is not pressed, handle it
if (ev.keyCode == 13 && !ev.shiftKey && this.editor._iframe.contentWindow.getSelection)
{
return this.handleEnter(ev);
}
}; // end of _onKeyPress()
// -----------------------------------------------------------------------------------
/**
* Handles the pressing of an unshifted enter for Gecko
*/
EnterParagraphs.prototype.handleEnter = function(ev)
{
var cursorNode;
// Grab the selection and associated range
var sel = this.editor.getSelection();
var rng = this.editor.createRange(sel);
// if we are at the end of a list and the node is empty let the browser handle
// it to get us out of the list.
if ( this.isNormalListItem(rng) )
{
return true;
}
// as far as I can tell this isn't actually used.
this.takenIds = new Object();
// Grab ranges for document re-stuffing, if appropriate
//
// pStart and pEnd are arrays consisting of
// [0] neighbor node
// [1] insertion type
// [2] roam
var pStart = this.processSide(rng, "left");
var pEnd = this.processSide(rng, "right");
// used to position the cursor after insertion.
cursorNode = pEnd[2];
// Get rid of everything local to the selection
sel.removeAllRanges();
rng.deleteContents();
// Grab a node we'll have after insertion, since fragments will be lost
//
// we'll use this to position the cursor.
var holdEnd = this.forEachNodeUnder( cursorNode, "find_cursorpoint", "ltr", false, true);
if ( ! holdEnd )
{
alert( "INTERNAL ERROR - could not find place to put cursor after ENTER" );
}
// Insert our carefully chosen document fragments
if ( pStart )
{
this.insertAdjacentElement(pStart[0], pStart[1], pStart[2]);
}
if ( pEnd && pEnd.nodeType != 1)
{
this.insertAdjacentElement(pEnd[0], pEnd[1], pEnd[2]);
}
// Move the caret in front of the first good text element
if ((holdEnd) && (this._permEmpty.test(holdEnd.nodeName) ))
{
var prodigal = 0;
while ( holdEnd.parentNode.childNodes.item(prodigal) != holdEnd )
{
prodigal++;
}
sel.collapse( holdEnd.parentNode, prodigal);
}
else
{
// holdEnd might be false.
try
{
sel.collapse(holdEnd, 0);
// interestingly, scrollToElement() scroll so the top if holdEnd is a text node.
if ( holdEnd.nodeType == 3 )
{
holdEnd = holdEnd.parentNode;
}
this.editor.scrollElementIntoViewport(holdEnd);
//this.editor.scrollToElement(holdEnd);
}
catch (e)
{
// we could try to place the cursor at the end of the document.
}
}
this.editor.updateToolbar();
Xinha._stopEvent(ev);
return true;
}; // end of handleEnter()
// END