Changeset 6


Ignore:
Timestamp:
02/13/05 07:43:25 (15 years ago)
Author:
gogo
Message:

New version from upstream (hipikat on the htmlarea.com forums).

Note that a new config option is added, which allows turning this on automagically.

Location:
trunk/plugins/EnterParagraphs
Files:
2 added
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/plugins/EnterParagraphs/enter-paragraphs.js

    r1 r6  
    1 // Modification to htmlArea to insert Paragraphs instead of 
    2 // linebreaks, under Gecko engines, circa January 2004 
    31// By Adam Wright, for The University of Western Australia 
    42// 
     
    64// This notice MUST stay intact for use (see license.txt). 
    75 
    8 function EnterParagraphs(editor, params) { 
     6function EnterParagraphs(editor) { 
    97  this.editor = editor; 
    10   // activate only if we're talking to Gecko 
     8 
     9  // Activate only if we're talking to Gecko 
    1110  if (HTMLArea.is_gecko) 
    1211    this.onKeyPress = this.__onKeyPress; 
     
    1716  version       : "1.0", 
    1817  developer     : "Adam Wright", 
    19   developer_url : "http://blog.hipikat.org/", 
     18  developer_url : "http://www.hipikat.org/", 
    2019  sponsor       : "The University of Western Australia", 
    2120  sponsor_url   : "http://www.uwa.edu.au/", 
     
    2322}; 
    2423 
    25 // An array of elements who, in html4, by default, have an inline display and can have children 
    26 // we use RegExp here since it should be a bit faster, also cleaner to check 
    27 EnterParagraphs.prototype._html4_inlines_re = /^(a|abbr|acronym|b|bdo|big|cite|code|dfn|em|font|i|kbd|label|q|s|samp|select|small|span|strike|strong|sub|sup|textarea|tt|u|var)$/i; 
    28  
    29 // Finds the first parent element of a given node whose display is probably not inline 
    30 EnterParagraphs.prototype.parentBlock = function(node) { 
    31   while (node.parentNode && (node.nodeType != 1 || this._html4_inlines_re.test(node.tagName))) 
    32     node = node.parentNode; 
    33   return node; 
    34 }; 
    35  
    36 // Internal function for recursively itterating over a all nodes in a fragment 
    37 // If a callback function returns a non-null value, that is returned and the crawl is therefore broken 
    38 EnterParagraphs.prototype.walkNodeChildren = function(me, callback) { 
    39   if (me.firstChild) { 
    40     var myChild = me.firstChild; 
    41     var retVal; 
    42     while (myChild) { 
    43       if ((retVal = callback(this, myChild)) != null) 
    44         return retVal; 
    45       if ((retVal = this.walkNodeChildren(myChild, callback)) != null) 
    46         return retVal; 
    47       myChild = myChild.nextSibling; 
    48     } 
    49   } 
    50 }; 
    51  
    52 // Callback function to be performed on each node in the hierarchy 
    53 // Sets flag to true if we find actual text or an element that's not usually displayed inline 
    54 EnterParagraphs.prototype._isFilling = function(self, node) { 
    55   if (node.nodeType == 1 && !self._html4_inlines_re.test(node.nodeName)) 
     24// Whitespace Regex 
     25EnterParagraphs.prototype._whiteSpace = /^\s*$/; 
     26// The pragmatic list of which elements a paragraph may not contain, and which may contain a paragraph 
     27EnterParagraphs.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; 
     28EnterParagraphs.prototype._pContainers = /^(body|del|div|fieldset|form|ins|map|noscript|object|td|th)$/i; 
     29// Elements which may not contain paragraphs, and would prefer a break to being split 
     30EnterParagraphs.prototype._pBreak = /^(address|pre|blockquote)$/i; 
     31// Elements which may not contain children 
     32EnterParagraphs.prototype._permEmpty = /^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i; 
     33// Elements which count as content, as distinct from whitespace or containers 
     34EnterParagraphs.prototype._elemSolid = /^(applet|br|button|hr|img|input|table)$/i; 
     35// Elements which should get a new P, before or after, when enter is pressed at either end 
     36EnterParagraphs.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; 
     37EnterParagraphs.prototype._pifyForced = /^(ul|ol|dl|table)$/i; 
     38// Elements which should get a new P, before or after a close parent, when enter is pressed at either end 
     39EnterParagraphs.prototype._pifyParent = /^(dd|dt|li|td|th|tr)$/i; 
     40 
     41// Gecko's a bit lacking in some odd ways... 
     42EnterParagraphs.prototype.insertAdjacentElement = function(ref,pos,el) { 
     43 
     44  if ( pos == 'BeforeBegin' ) ref.parentNode.insertBefore(el,ref); 
     45  else if ( pos == 'AfterEnd' ) ref.nextSibling ? ref.parentNode.insertBefore(el,ref.nextSibling) : ref.parentNode.appendChild(el); 
     46  else if ( pos == 'AfterBegin' && ref.firstChild ) ref.insertBefore(el,ref.firstChild); 
     47  else if ( pos == 'BeforeEnd' || pos == 'AfterBegin' ) ref.appendChild(el); 
     48}; 
     49 
     50// Passes a global parent node or document fragment to forEachNode 
     51EnterParagraphs.prototype.forEachNodeUnder = function (top, fn, ltr, init, parm) { 
     52 
     53  // Identify the first and last nodes to deal with 
     54  var start, end; 
     55  if ( top.nodeType == 11 && top.firstChild ) { 
     56    start = top.firstChild; 
     57    end = top.lastChild; 
     58  } else start = end = top; 
     59  while ( end.lastChild ) end = end.lastChild; 
     60 
     61  // Pass onto forEachNode 
     62  return this.forEachNode(start, end, fn, ltr, init, parm); 
     63}; 
     64 
     65// Throws each node into a function 
     66EnterParagraphs.prototype.forEachNode = function (left, right, fn, ltr, init, parm) { 
     67 
     68  var xBro = function(elem, ltr) { return ( ltr ? elem.nextSibling : elem.previousSibling ); }; 
     69  var xSon = function(elem, ltr) { return ( ltr ? elem.firstChild : elem.lastChild ); }; 
     70  var walk, lookup, fnVal, ping = init; 
     71 
     72  // Until we've hit the last node 
     73  while ( walk != ltr ? right : left ) { 
     74 
     75    // Progress to the next node 
     76    if ( !walk ) walk = ltr ? left : right; 
     77    else { 
     78      if ( xSon(walk,ltr) ) walk = xSon(walk,ltr); 
     79      else { 
     80        if ( xBro(walk,ltr) ) walk = xBro(walk,ltr); 
     81        else { 
     82          lookup = walk; 
     83          while ( !xBro(lookup,ltr) && lookup != (ltr ? right : left) ) lookup = lookup.parentNode; 
     84          walk = ( lookup.nextSibling ? lookup.nextSibling : lookup ) ; 
     85          if ( walk == right ) break; 
     86    }   }       } 
     87 
     88    fnVal = fn(this, walk, ping, parm, (walk==(ltr?right:left)));       // Throw this node at the wanted function 
     89    if ( fnVal[0] ) return fnVal[1];                                                            // If this node wants us to return, return pong 
     90    if ( fnVal[1] ) ping = fnVal[1];                                                            // Otherwise, set pong to ping, to pass to the next node 
     91  } 
     92  return false; 
     93}; 
     94 
     95// forEachNode fn: Find a post-insertion node, only if all nodes are empty, or the first content 
     96EnterParagraphs.prototype._fenEmptySet = function (parent, node, pong, getCont, last) { 
     97 
     98  // Mark this if it's the first base 
     99  if ( !pong && !node.firstChild ) pong = node; 
     100 
     101  // Check for content 
     102  if ( (node.nodeType == 1 && parent._elemSolid.test(node.nodeName)) || 
     103    (node.nodeType == 3 && !parent._whiteSpace.test(node.nodeValue)) || 
     104    (node.nodeType != 1 && node.nodeType != 3) ) { 
     105 
     106    return new Array(true, (getCont?node:false)); 
     107  } 
     108 
     109  // Only return the 'base' node if we didn't want content 
     110  if ( last && !getCont ) return new Array(true, pong); 
     111  return new Array(false, pong); 
     112}; 
     113 
     114// forEachNode fn: 
     115EnterParagraphs.prototype._fenCullIds = function (parent, node, pong, parm, last) { 
     116 
     117  // Check for an id, blast it if it's in the store, otherwise add it 
     118  if ( node.id ) pong[node.id] ? node.id = '' : pong[node.id] = true; 
     119  return new Array(false,pong); 
     120}; 
     121 
     122// Grabs a range suitable for paragraph stuffing 
     123EnterParagraphs.prototype.processSide = function(rng, left) { 
     124 
     125  var next = function(element, left) { return ( left ? element.previousSibling : element.nextSibling ); }; 
     126  var node = left ? rng.startContainer : rng.endContainer; 
     127  var offset = left ? rng.startOffset : rng.endOffset; 
     128  var roam, start = node; 
     129 
     130  // Never start with an element, because then the first roaming node might 
     131  // be on the exclusion list and we wouldn't know until it was too late 
     132  while ( start.nodeType == 1 && !this._permEmpty.test(start.nodeName) ) start = ( offset ? start.lastChild : start.firstChild ); 
     133 
     134  // Climb the tree, left or right, until our course of action presents itself 
     135  while ( roam = roam ? ( next(roam,left) ? next(roam,left) : roam.parentNode ) : start ) { 
     136 
     137    if ( next(roam,left) ) { 
     138      // If the next sibling's on the exclusion list, stop before it 
     139      if ( this._pExclusions.test(next(roam,left).nodeName) ) { 
     140        return this.processRng(rng, left, roam, next(roam,left), (left?'AfterEnd':'BeforeBegin'), true, false); 
     141    } } else { 
     142      // If our parent's on the container list, stop inside it 
     143      if (this._pContainers.test(roam.parentNode.nodeName)) { 
     144        return this.processRng(rng, left, roam, roam.parentNode, (left?'AfterBegin':'BeforeEnd'), true, false); 
     145      } 
     146      // If our parent's on the exclusion list, chop without wrapping 
     147      else if (this._pExclusions.test(roam.parentNode.nodeName)) { 
     148        if (this._pBreak.test(roam.parentNode.nodeName)) { 
     149          return this.processRng(rng, left, roam, roam.parentNode, 
     150                            (left?'AfterBegin':'BeforeEnd'), false, (left?true:false)); 
     151        } else { 
     152          return this.processRng(rng, left, (roam = roam.parentNode), 
     153                            (next(roam,left) ? next(roam,left) : roam.parentNode), 
     154              (next(roam,left) ? (left?'AfterEnd':'BeforeBegin') : (left?'AfterBegin':'BeforeEnd')), false, false); 
     155}       }       }       }       }; 
     156 
     157// Neighbour and insertion identify where the new node, roam, needs to enter 
     158// the document; landmarks in our selection will be deleted before insertion 
     159EnterParagraphs.prototype.processRng = function(rng, left, roam, neighbour, insertion, pWrap, preBr) { 
     160 
     161  var node = left ? rng.startContainer : rng.endContainer; 
     162  var offset = left ? rng.startOffset : rng.endOffset; 
     163 
     164  // Define the range to cut, and extend the selection range to the same boundary 
     165  var editor = this.editor; 
     166  var newRng = editor._doc.createRange(); 
     167  newRng.selectNode(roam); 
     168  if (left) { 
     169    newRng.setEnd(node, offset); 
     170    rng.setStart(newRng.startContainer, newRng.startOffset); 
     171  } else { 
     172    newRng.setStart(node, offset); 
     173    rng.setEnd(newRng.endContainer, newRng.endOffset); 
     174  } 
     175 
     176  // Clone the range and remove duplicate ids it would otherwise produce 
     177  var cnt = newRng.cloneContents(); 
     178  this.forEachNodeUnder(cnt, this._fenCullIds, true, this.takenIds, false); 
     179 
     180  // Special case, for inserting paragraphs before some blocks when caret is at their zero offset 
     181  var pify, pifyOffset, fill; 
     182  pify = left ? (newRng.endContainer.nodeType == 3 ? true:false) : (newRng.startContainer.nodeType == 3 ? false:true); 
     183  pifyOffset = pify ? newRng.startOffset : newRng.endOffset; 
     184  pify = pify ? newRng.startContainer : newRng.endContainer; 
     185 
     186  if ( this._pifyParent.test(pify.nodeName) && pify.parentNode.childNodes.item(0) == pify ) { 
     187    while ( !this._pifySibling.test(pify.nodeName) ) pify = pify.parentNode; 
     188  } 
     189 
     190  if ( cnt.nodeType == 11 && !cnt.firstChild ) cnt.appendChild(editor._doc.createElement(pify.nodeName)); 
     191  fill = this.forEachNodeUnder(cnt,this._fenEmptySet,true,false,false); 
     192 
     193  if ( fill && this._pifySibling.test(pify.nodeName) && 
     194    ( (pifyOffset == 0) || ( pifyOffset == 1 && this._pifyForced.test(pify.nodeName) ) ) ) { 
     195 
     196    roam = editor._doc.createElement('p'); 
     197    roam.appendChild(editor._doc.createElement('br')); 
     198 
     199    if (left && pify.previousSibling) return new Array(pify.previousSibling, 'AfterEnd', roam); 
     200    else if (!left && pify.nextSibling) return new Array(pify.nextSibling, 'BeforeBegin', roam); 
     201    else return new Array(pify.parentNode, (left?'AfterBegin':'BeforeEnd'), roam); 
     202  } 
     203 
     204  // If our cloned contents are 'content'-less, shove a break in them 
     205  if ( fill ) { 
     206    if ( fill.nodeType == 3 ) fill = fill.parentNode;           // Ill-concieved? 
     207    if ( (fill.nodeType == 1 && !this._elemSolid.test()) || fill.nodeType == 11 ) fill.appendChild(editor._doc.createElement('br')); 
     208    else fill.parentNode.insertBefore(editor._doc.createElement('br'),fill); 
     209  } 
     210 
     211  // And stuff a shiny new object with whatever contents we have 
     212  roam = (pWrap || (cnt.nodeType == 11 && !cnt.firstChild)) ? editor._doc.createElement('p') : editor._doc.createDocumentFragment(); 
     213  roam.appendChild(cnt); 
     214  if (preBr) roam.appendChild(editor._doc.createElement('br')); 
     215 
     216  // Return the nearest relative, relative insertion point and fragment to insert 
     217  return new Array(neighbour, insertion, roam); 
     218}; 
     219 
     220// Called when a key is pressed in the editor 
     221EnterParagraphs.prototype.__onKeyPress = function(ev) { 
     222 
     223  // If they've hit enter and shift is up, take it 
     224  if (ev.keyCode == 13 && !ev.shiftKey && this.editor._iframe.contentWindow.getSelection) 
     225    return this.handleEnter(ev); 
     226}; 
     227 
     228// Handles the pressing of an unshifted enter for Gecko 
     229EnterParagraphs.prototype.handleEnter = function(ev) { 
     230 
     231  // Grab the selection and associated range 
     232  var sel = this.editor._getSelection(); 
     233  var rng = this.editor._createRange(sel); 
     234  this.takenIds = new Object(); 
     235 
     236  // Grab ranges for document re-stuffing, if appropriate 
     237  var pStart = this.processSide(rng, true); 
     238  var pEnd = this.processSide(rng, false); 
     239 
     240  // Get rid of everything local to the selection 
     241  sel.removeAllRanges(); 
     242  rng.deleteContents(); 
     243 
     244  // Grab a node we'll have after insertion, since fragments will be lost 
     245  var holdEnd = this.forEachNodeUnder(pEnd[2], this._fenEmptySet, true, false, true); 
     246 
     247  // Reinsert our carefully chosen document fragments 
     248  if ( pStart ) this.insertAdjacentElement(pStart[0], pStart[1], pStart[2]); 
     249  if ( pEnd.nodeType != 1 ) this.insertAdjacentElement(pEnd[0], pEnd[1], pEnd[2]); 
     250 
     251  // Move the caret in front of the first good text element 
     252  if ( this._permEmpty.test(holdEnd.nodeName) ) { 
     253    var prodigal = 0; 
     254    while ( holdEnd.parentNode.childNodes.item(prodigal) != holdEnd ) prodigal++; 
     255    sel.collapse( holdEnd.parentNode, prodigal); 
     256  } 
     257  else sel.collapse(holdEnd, 0); 
     258  editor.scrollToElement(holdEnd); 
     259  editor.updateToolbar(); 
     260 
     261  //====================== 
     262    HTMLArea._stopEvent(ev); 
    56263    return true; 
    57   else if (node.nodeType == 3 && node.nodeValue != '') 
    58     return true; 
    59   return null; 
    60   //alert(node.nodeName); 
    61 }; 
    62  
    63 // Inserts a node deeply on the left of a hierarchy of nodes 
    64 EnterParagraphs.prototype.insertDeepLeftText = function(target, toInsert) { 
    65   var falling = target; 
    66   while (falling.firstChild && falling.firstChild.nodeType == 1) 
    67     falling = falling.firstChild; 
    68   //var refNode = falling.firstChild ? falling.firstChild : null; 
    69   //falling.insertBefore(toInsert, refNode); 
    70   falling.innerHTML = toInsert; 
    71 }; 
    72  
    73 // Kind of like a macros, for a frequent query... 
    74 EnterParagraphs.prototype.isElem = function(node, type) { 
    75   return node.nodeName.toLowerCase() == type.toLowerCase(); 
    76 }; 
    77  
    78 // The onKeyPress even that does all the work - nicely breaks the line into paragraphs 
    79 EnterParagraphs.prototype.__onKeyPress = function(ev) { 
    80  
    81   if (ev.keyCode == 13 && !ev.shiftKey && this.editor._iframe.contentWindow.getSelection) { 
    82  
    83     var editor = this.editor; 
    84  
    85     // Get the selection and solid references to what we're dealing with chopping 
    86     var sel = editor._iframe.contentWindow.getSelection(); 
    87  
    88     // Set the start and end points such that they're going /forward/ through the document 
    89     var rngLeft = editor._doc.createRange();            var rngRight = editor._doc.createRange(); 
    90     rngLeft.setStart(sel.anchorNode, sel.anchorOffset); rngRight.setStart(sel.focusNode, sel.focusOffset); 
    91     rngLeft.collapse(true);                                     rngRight.collapse(true); 
    92  
    93     var direct = rngLeft.compareBoundaryPoints(rngLeft.START_TO_END, rngRight) < 0; 
    94  
    95     var startNode = direct ? sel.anchorNode : sel.focusNode; 
    96     var startOffset = direct ? sel.anchorOffset : sel.focusOffset; 
    97     var endNode = direct ? sel.focusNode : sel.anchorNode; 
    98     var endOffset = direct ? sel.focusOffset : sel.anchorOffset; 
    99  
    100     // Find the parent blocks of nodes at either end, and their attributes if they're paragraphs 
    101     var startBlock = this.parentBlock(startNode);               var endBlock = this.parentBlock(endNode); 
    102     var attrsLeft = new Array();                                var attrsRight = new Array(); 
    103  
    104     // If a list, let the browser take over, if we're in a paragraph, gather it's attributes 
    105     if (this.isElem(startBlock, 'li') || this.isElem(endBlock, 'li')) 
    106       return; 
    107  
    108     if (this.isElem(startBlock, 'p')) { 
    109       for (var i = 0; i < startBlock.attributes.length; i++) { 
    110         attrsLeft[startBlock.attributes[i].nodeName] = startBlock.attributes[i].nodeValue; 
    111       } 
    112     } 
    113     if (this.isElem(endBlock, 'p')) { 
    114       for (var i = 0; i < endBlock.attributes.length; i++) { 
    115         // If we start and end within one paragraph, don't duplicate the 'id' 
    116         if (endBlock != startBlock || endBlock.attributes[i].nodeName.toLowerCase() != 'id') 
    117           attrsRight[endBlock.attributes[i].nodeName] = endBlock.attributes[i].nodeValue; 
    118       } 
    119     } 
    120  
    121     // Look for where to start and end our chopping - within surrounding paragraphs 
    122     // if they exist, or at the edges of the containing block, otherwise 
    123     var startChop = startNode;                          var endChop = endNode; 
    124  
    125     while ((startChop.previousSibling && !this.isElem(startChop.previousSibling, 'p')) 
    126            || (startChop.parentNode && startChop.parentNode != startBlock && startChop.parentNode.nodeType != 9)) 
    127       startChop = startChop.previousSibling ? startChop.previousSibling : startChop.parentNode; 
    128  
    129     while ((endChop.nextSibling && !this.isElem(endChop.nextSibling, 'p')) 
    130            || (endChop.parentNode && endChop.parentNode != endBlock && endChop.parentNode.nodeType != 9)) 
    131       endChop = endChop.nextSibling ? endChop.nextSibling : endChop.parentNode; 
    132  
    133     // Set up new paragraphs 
    134     var pLeft = editor._doc.createElement('p');         var pRight = editor._doc.createElement('p'); 
    135  
    136     for (var attrName in attrsLeft) { 
    137       var thisAttr = editor._doc.createAttribute(attrName); 
    138       thisAttr.value = attrsLeft[attrName]; 
    139       pLeft.setAttributeNode(thisAttr); 
    140     } 
    141     for (var attrName in attrsRight) { 
    142       var thisAttr = editor._doc.createAttribute(attrName); 
    143       thisAttr.value = attrsRight[attrName]; 
    144       pRight.setAttributeNode(thisAttr); 
    145     } 
    146  
    147     // Get the ranges destined to be stuffed into new paragraphs 
    148     rngLeft.setStartBefore(startChop); 
    149     rngLeft.setEnd(startNode,startOffset); 
    150     pLeft.appendChild(rngLeft.cloneContents());         // Copy into pLeft 
    151  
    152     rngRight.setEndAfter(endChop); 
    153     rngRight.setStart(endNode,endOffset); 
    154     pRight.appendChild(rngRight.cloneContents());               // Copy into pRight 
    155  
    156     // If either paragraph is empty, fill it with a nonbreakable space 
    157     var foundBlock = false; 
    158     foundBlock = this.walkNodeChildren(pLeft, this._isFilling); 
    159     if (foundBlock != true) 
    160       this.insertDeepLeftText(pLeft, '&nbsp;'); 
    161  
    162     foundBlock = false; 
    163     foundBlock = this.walkNodeChildren(pRight, this._isFilling); 
    164     if (foundBlock != true) 
    165       this.insertDeepLeftText(pRight, '&nbsp;'); 
    166  
    167     // Get a range for everything to be replaced and replace it 
    168     var rngAround = editor._doc.createRange(); 
    169  
    170     if (!startChop.previousSibling && this.isElem(startChop.parentNode, 'p')) 
    171       rngAround.setStartBefore(startChop.parentNode); 
    172     else 
    173       rngAround.setStart(rngLeft.startContainer, rngLeft.startOffset); 
    174  
    175     if (!endChop.nextSibling && this.isElem(endChop.parentNode, 'p')) 
    176       rngAround.setEndAfter(endChop.parentNode); 
    177     else 
    178       rngAround.setEnd(rngRight.endContainer, rngRight.endOffset); 
    179  
    180     rngAround.deleteContents(); 
    181     rngAround.insertNode(pRight); 
    182     rngAround.insertNode(pLeft); 
    183  
    184     // Set the selection to the start of the (second) new paragraph 
    185     if (pRight.firstChild) { 
    186       while (pRight.firstChild && this._html4_inlines_re.test(pRight.firstChild.nodeName)) 
    187         pRight = pRight.firstChild; 
    188       // Slip into any inline tags 
    189       if (pRight.firstChild && pRight.firstChild.nodeType == 3) 
    190         pRight = pRight.firstChild;     // and text, if they've got it 
    191  
    192       var rngCaret = editor._doc.createRange(); 
    193       rngCaret.setStart(pRight, 0); 
    194       rngCaret.collapse(true); 
    195  
    196       sel = editor._iframe.contentWindow.getSelection(); 
    197       sel.removeAllRanges(); 
    198       sel.addRange(rngCaret); 
    199     } 
    200  
    201     // Stop the bubbling 
    202     HTMLArea._stopEvent(ev); 
    203   } 
    204 }; 
     264}; 
Note: See TracChangeset for help on using the changeset viewer.