define("dijit/_editor/plugins/EnterKeyHandling", [ "dojo/_base/declare", // declare "dojo/dom-construct", // domConstruct.destroy domConstruct.place "dojo/_base/event", // event.stop "dojo/keys", // keys.ENTER "dojo/_base/lang", "dojo/_base/sniff", // has("ie") has("mozilla") has("webkit") "dojo/_base/window", // win.global win.withGlobal "dojo/window", // winUtils.scrollIntoView "../_Plugin", "../RichText", "../range", "../selection" ], function(declare, domConstruct, event, keys, lang, has, win, winUtils, _Plugin, RichText, rangeapi, selectionapi){ /*===== var _Plugin = dijit._editor._Plugin; =====*/ // module: // dijit/_editor/plugins/EnterKeyHandling // summary: // This plugin tries to make all browsers behave consistently with regard to // how ENTER behaves in the editor window. It traps the ENTER key and alters // the way DOM is constructed in certain cases to try to commonize the generated // DOM and behaviors across browsers. return declare("dijit._editor.plugins.EnterKeyHandling", _Plugin, { // summary: // This plugin tries to make all browsers behave consistently with regard to // how ENTER behaves in the editor window. It traps the ENTER key and alters // the way DOM is constructed in certain cases to try to commonize the generated // DOM and behaviors across browsers. // // description: // This plugin has three modes: // // * blockNodeForEnter=BR // * blockNodeForEnter=DIV // * blockNodeForEnter=P // // In blockNodeForEnter=P, the ENTER key starts a new // paragraph, and shift-ENTER starts a new line in the current paragraph. // For example, the input: // // | first paragraph // | second line of first paragraph // | second paragraph // // will generate: // // |

// | first paragraph // |
// | second line of first paragraph // |

// |

// | second paragraph // |

// // In BR and DIV mode, the ENTER key conceptually goes to a new line in the // current paragraph, and users conceptually create a new paragraph by pressing ENTER twice. // For example, if the user enters text into an editor like this: // // | one // | two // | three // | // | four // | five // | six // // It will appear on the screen as two 'paragraphs' of three lines each. Markupwise, this generates: // // BR: // | one
// | two
// | three
// |
// | four
// | five
// | six
// // DIV: // |
one
// |
two
// |
three
// |
 
// |
four
// |
five
// |
six
// blockNodeForEnter: String // This property decides the behavior of Enter key. It can be either P, // DIV, BR, or empty (which means disable this feature). Anything else // will trigger errors. The default is 'BR' // // See class description for more details. blockNodeForEnter: 'BR', constructor: function(args){ if(args){ if("blockNodeForEnter" in args){ args.blockNodeForEnter = args.blockNodeForEnter.toUpperCase(); } lang.mixin(this,args); } }, setEditor: function(editor){ // Overrides _Plugin.setEditor(). if(this.editor === editor){ return; } this.editor = editor; if(this.blockNodeForEnter == 'BR'){ // While Moz has a mode tht mostly works, it's still a little different, // So, try to just have a common mode and be consistent. Which means // we need to enable customUndo, if not already enabled. this.editor.customUndo = true; editor.onLoadDeferred.then(lang.hitch(this,function(d){ this.connect(editor.document, "onkeypress", function(e){ if(e.charOrCode == keys.ENTER){ // Just do it manually. The handleEnterKey has a shift mode that // Always acts like
, so just use it. var ne = lang.mixin({},e); ne.shiftKey = true; if(!this.handleEnterKey(ne)){ event.stop(e); } } }); if(has("ie") == 9){ this.connect(editor.document, "onpaste", function(e){ setTimeout(dojo.hitch(this, function(){ // Use the old range/selection code to kick IE 9 into updating // its range by moving it back, then forward, one 'character'. var r = this.editor.document.selection.createRange(); r.move('character',-1); r.select(); r.move('character',1); r.select(); }),0); }); } return d; })); }else if(this.blockNodeForEnter){ // add enter key handler // FIXME: need to port to the new event code!! var h = lang.hitch(this,this.handleEnterKey); editor.addKeyHandler(13, 0, 0, h); //enter editor.addKeyHandler(13, 0, 1, h); //shift+enter this.connect(this.editor,'onKeyPressed','onKeyPressed'); } }, onKeyPressed: function(){ // summary: // Handler for keypress events. // tags: // private if(this._checkListLater){ if(win.withGlobal(this.editor.window, 'isCollapsed', dijit)){ var liparent=win.withGlobal(this.editor.window, 'getAncestorElement', selectionapi, ['LI']); if(!liparent){ // circulate the undo detection code by calling RichText::execCommand directly RichText.prototype.execCommand.call(this.editor, 'formatblock',this.blockNodeForEnter); // set the innerHTML of the new block node var block = win.withGlobal(this.editor.window, 'getAncestorElement', selectionapi, [this.blockNodeForEnter]); if(block){ block.innerHTML=this.bogusHtmlContent; if(has("ie")){ // move to the start by moving backwards one char var r = this.editor.document.selection.createRange(); r.move('character',-1); r.select(); } }else{ console.error('onKeyPressed: Cannot find the new block node'); // FIXME } }else{ if(has("mozilla")){ if(liparent.parentNode.parentNode.nodeName == 'LI'){ liparent=liparent.parentNode.parentNode; } } var fc=liparent.firstChild; if(fc && fc.nodeType == 1 && (fc.nodeName == 'UL' || fc.nodeName == 'OL')){ liparent.insertBefore(fc.ownerDocument.createTextNode('\xA0'),fc); var newrange = rangeapi.create(this.editor.window); newrange.setStart(liparent.firstChild,0); var selection = rangeapi.getSelection(this.editor.window, true); selection.removeAllRanges(); selection.addRange(newrange); } } } this._checkListLater = false; } if(this._pressedEnterInBlock){ // the new created is the original current P, so we have previousSibling below if(this._pressedEnterInBlock.previousSibling){ this.removeTrailingBr(this._pressedEnterInBlock.previousSibling); } delete this._pressedEnterInBlock; } }, // bogusHtmlContent: [private] String // HTML to stick into a new empty block bogusHtmlContent: ' ', //   // blockNodes: [private] Regex // Regex for testing if a given tag is a block level (display:block) tag blockNodes: /^(?:P|H1|H2|H3|H4|H5|H6|LI)$/, handleEnterKey: function(e){ // summary: // Handler for enter key events when blockNodeForEnter is DIV or P. // description: // Manually handle enter key event to make the behavior consistent across // all supported browsers. See class description for details. // tags: // private var selection, range, newrange, startNode, endNode, brNode, doc=this.editor.document,br,rs,txt; if(e.shiftKey){ // shift+enter always generates
var parent = win.withGlobal(this.editor.window, "getParentElement", selectionapi); var header = rangeapi.getAncestor(parent,this.blockNodes); if(header){ if(header.tagName == 'LI'){ return true; // let browser handle } selection = rangeapi.getSelection(this.editor.window); range = selection.getRangeAt(0); if(!range.collapsed){ range.deleteContents(); selection = rangeapi.getSelection(this.editor.window); range = selection.getRangeAt(0); } if(rangeapi.atBeginningOfContainer(header, range.startContainer, range.startOffset)){ br=doc.createElement('br'); newrange = rangeapi.create(this.editor.window); header.insertBefore(br,header.firstChild); newrange.setStartAfter(br); selection.removeAllRanges(); selection.addRange(newrange); }else if(rangeapi.atEndOfContainer(header, range.startContainer, range.startOffset)){ newrange = rangeapi.create(this.editor.window); br=doc.createElement('br'); header.appendChild(br); header.appendChild(doc.createTextNode('\xA0')); newrange.setStart(header.lastChild,0); selection.removeAllRanges(); selection.addRange(newrange); }else{ rs = range.startContainer; if(rs && rs.nodeType == 3){ // Text node, we have to split it. txt = rs.nodeValue; win.withGlobal(this.editor.window, function(){ startNode = doc.createTextNode(txt.substring(0, range.startOffset)); endNode = doc.createTextNode(txt.substring(range.startOffset)); brNode = doc.createElement("br"); if(endNode.nodeValue == "" && has("webkit")){ endNode = doc.createTextNode('\xA0') } domConstruct.place(startNode, rs, "after"); domConstruct.place(brNode, startNode, "after"); domConstruct.place(endNode, brNode, "after"); domConstruct.destroy(rs); newrange = rangeapi.create(); newrange.setStart(endNode,0); selection.removeAllRanges(); selection.addRange(newrange); }); return false; } return true; // let browser handle } }else{ selection = rangeapi.getSelection(this.editor.window); if(selection.rangeCount){ range = selection.getRangeAt(0); if(range && range.startContainer){ if(!range.collapsed){ range.deleteContents(); selection = rangeapi.getSelection(this.editor.window); range = selection.getRangeAt(0); } rs = range.startContainer; if(rs && rs.nodeType == 3){ // Text node, we have to split it. win.withGlobal(this.editor.window, lang.hitch(this, function(){ var endEmpty = false; var offset = range.startOffset; if(rs.length < offset){ //We are not splitting the right node, try to locate the correct one ret = this._adjustNodeAndOffset(rs, offset); rs = ret.node; offset = ret.offset; } txt = rs.nodeValue; startNode = doc.createTextNode(txt.substring(0, offset)); endNode = doc.createTextNode(txt.substring(offset)); brNode = doc.createElement("br"); if(!endNode.length){ endNode = doc.createTextNode('\xA0'); endEmpty = true; } if(startNode.length){ domConstruct.place(startNode, rs, "after"); }else{ startNode = rs; } domConstruct.place(brNode, startNode, "after"); domConstruct.place(endNode, brNode, "after"); domConstruct.destroy(rs); newrange = rangeapi.create(); newrange.setStart(endNode,0); newrange.setEnd(endNode, endNode.length); selection.removeAllRanges(); selection.addRange(newrange); if(endEmpty && !has("webkit")){ selectionapi.remove(); }else{ selectionapi.collapse(true); } })); }else{ var targetNode; if(range.startOffset >= 0){ targetNode = rs.childNodes[range.startOffset]; } win.withGlobal(this.editor.window, lang.hitch(this, function(){ var brNode = doc.createElement("br"); var endNode = doc.createTextNode('\xA0'); if(!targetNode){ rs.appendChild(brNode); rs.appendChild(endNode); }else{ domConstruct.place(brNode, targetNode, "before"); domConstruct.place(endNode, brNode, "after"); } newrange = rangeapi.create(win.global); newrange.setStart(endNode,0); newrange.setEnd(endNode, endNode.length); selection.removeAllRanges(); selection.addRange(newrange); selectionapi.collapse(true); })); } } }else{ // don't change this: do not call this.execCommand, as that may have other logic in subclass RichText.prototype.execCommand.call(this.editor, 'inserthtml', '
'); } } return false; } var _letBrowserHandle = true; // first remove selection selection = rangeapi.getSelection(this.editor.window); range = selection.getRangeAt(0); if(!range.collapsed){ range.deleteContents(); selection = rangeapi.getSelection(this.editor.window); range = selection.getRangeAt(0); } var block = rangeapi.getBlockAncestor(range.endContainer, null, this.editor.editNode); var blockNode = block.blockNode; // if this is under a LI or the parent of the blockNode is LI, just let browser to handle it if((this._checkListLater = (blockNode && (blockNode.nodeName == 'LI' || blockNode.parentNode.nodeName == 'LI')))){ if(has("mozilla")){ // press enter in middle of P may leave a trailing
, let's remove it later this._pressedEnterInBlock = blockNode; } // if this li only contains spaces, set the content to empty so the browser will outdent this item if(/^(\s| | |\xA0|]*\bclass=['"]Apple-style-span['"][^>]*>(\s| | |\xA0)<\/span>)?(
)?$/.test(blockNode.innerHTML)){ // empty LI node blockNode.innerHTML = ''; if(has("webkit")){ // WebKit tosses the range when innerHTML is reset newrange = rangeapi.create(this.editor.window); newrange.setStart(blockNode, 0); selection.removeAllRanges(); selection.addRange(newrange); } this._checkListLater = false; // nothing to check since the browser handles outdent } return true; } // text node directly under body, let's wrap them in a node if(!block.blockNode || block.blockNode===this.editor.editNode){ try{ RichText.prototype.execCommand.call(this.editor, 'formatblock',this.blockNodeForEnter); }catch(e2){ /*squelch FF3 exception bug when editor content is a single BR*/ } // get the newly created block node // FIXME block = {blockNode:win.withGlobal(this.editor.window, "getAncestorElement", selectionapi, [this.blockNodeForEnter]), blockContainer: this.editor.editNode}; if(block.blockNode){ if(block.blockNode != this.editor.editNode && (!(block.blockNode.textContent || block.blockNode.innerHTML).replace(/^\s+|\s+$/g, "").length)){ this.removeTrailingBr(block.blockNode); return false; } }else{ // we shouldn't be here if formatblock worked block.blockNode = this.editor.editNode; } selection = rangeapi.getSelection(this.editor.window); range = selection.getRangeAt(0); } var newblock = doc.createElement(this.blockNodeForEnter); newblock.innerHTML=this.bogusHtmlContent; this.removeTrailingBr(block.blockNode); var endOffset = range.endOffset; var node = range.endContainer; if(node.length < endOffset){ //We are not checking the right node, try to locate the correct one var ret = this._adjustNodeAndOffset(node, endOffset); node = ret.node; endOffset = ret.offset; } if(rangeapi.atEndOfContainer(block.blockNode, node, endOffset)){ if(block.blockNode === block.blockContainer){ block.blockNode.appendChild(newblock); }else{ domConstruct.place(newblock, block.blockNode, "after"); } _letBrowserHandle = false; // lets move caret to the newly created block newrange = rangeapi.create(this.editor.window); newrange.setStart(newblock, 0); selection.removeAllRanges(); selection.addRange(newrange); if(this.editor.height){ winUtils.scrollIntoView(newblock); } }else if(rangeapi.atBeginningOfContainer(block.blockNode, range.startContainer, range.startOffset)){ domConstruct.place(newblock, block.blockNode, block.blockNode === block.blockContainer ? "first" : "before"); if(newblock.nextSibling && this.editor.height){ // position input caret - mostly WebKit needs this newrange = rangeapi.create(this.editor.window); newrange.setStart(newblock.nextSibling, 0); selection.removeAllRanges(); selection.addRange(newrange); // browser does not scroll the caret position into view, do it manually winUtils.scrollIntoView(newblock.nextSibling); } _letBrowserHandle = false; }else{ //press enter in the middle of P/DIV/Whatever/ if(block.blockNode === block.blockContainer){ block.blockNode.appendChild(newblock); }else{ domConstruct.place(newblock, block.blockNode, "after"); } _letBrowserHandle = false; // Clone any block level styles. if(block.blockNode.style){ if(newblock.style){ if(block.blockNode.style.cssText){ newblock.style.cssText = block.blockNode.style.cssText; } } } // Okay, we probably have to split. rs = range.startContainer; var firstNodeMoved; if(rs && rs.nodeType == 3){ // Text node, we have to split it. var nodeToMove, tNode; endOffset = range.endOffset; if(rs.length < endOffset){ //We are not splitting the right node, try to locate the correct one ret = this._adjustNodeAndOffset(rs, endOffset); rs = ret.node; endOffset = ret.offset; } txt = rs.nodeValue; startNode = doc.createTextNode(txt.substring(0, endOffset)); endNode = doc.createTextNode(txt.substring(endOffset, txt.length)); // Place the split, then remove original nodes. domConstruct.place(startNode, rs, "before"); domConstruct.place(endNode, rs, "after"); domConstruct.destroy(rs); // Okay, we split the text. Now we need to see if we're // parented to the block element we're splitting and if // not, we have to split all the way up. Ugh. var parentC = startNode.parentNode; while(parentC !== block.blockNode){ var tg = parentC.tagName; var newTg = doc.createElement(tg); // Clone over any 'style' data. if(parentC.style){ if(newTg.style){ if(parentC.style.cssText){ newTg.style.cssText = parentC.style.cssText; } } } // If font also need to clone over any font data. if(parentC.tagName === "FONT"){ if(parentC.color){ newTg.color = parentC.color; } if(parentC.face){ newTg.face = parentC.face; } if(parentC.size){ // this check was necessary on IE newTg.size = parentC.size; } } nodeToMove = endNode; while(nodeToMove){ tNode = nodeToMove.nextSibling; newTg.appendChild(nodeToMove); nodeToMove = tNode; } domConstruct.place(newTg, parentC, "after"); startNode = parentC; endNode = newTg; parentC = parentC.parentNode; } // Lastly, move the split out tags to the new block. // as they should now be split properly. nodeToMove = endNode; if(nodeToMove.nodeType == 1 || (nodeToMove.nodeType == 3 && nodeToMove.nodeValue)){ // Non-blank text and non-text nodes need to clear out that blank space // before moving the contents. newblock.innerHTML = ""; } firstNodeMoved = nodeToMove; while(nodeToMove){ tNode = nodeToMove.nextSibling; newblock.appendChild(nodeToMove); nodeToMove = tNode; } } //lets move caret to the newly created block newrange = rangeapi.create(this.editor.window); var nodeForCursor; var innerMostFirstNodeMoved = firstNodeMoved; if(this.blockNodeForEnter !== 'BR'){ while(innerMostFirstNodeMoved){ nodeForCursor = innerMostFirstNodeMoved; tNode = innerMostFirstNodeMoved.firstChild; innerMostFirstNodeMoved = tNode; } if(nodeForCursor && nodeForCursor.parentNode){ newblock = nodeForCursor.parentNode; newrange.setStart(newblock, 0); selection.removeAllRanges(); selection.addRange(newrange); if(this.editor.height){ winUtils.scrollIntoView(newblock); } if(has("mozilla")){ // press enter in middle of P may leave a trailing
, let's remove it later this._pressedEnterInBlock = block.blockNode; } }else{ _letBrowserHandle = true; } }else{ newrange.setStart(newblock, 0); selection.removeAllRanges(); selection.addRange(newrange); if(this.editor.height){ winUtils.scrollIntoView(newblock); } if(has("mozilla")){ // press enter in middle of P may leave a trailing
, let's remove it later this._pressedEnterInBlock = block.blockNode; } } } return _letBrowserHandle; }, _adjustNodeAndOffset: function(/*DomNode*/node, /*Int*/offset){ // summary: // In the case there are multiple text nodes in a row the offset may not be within the node. If the offset is larger than the node length, it will attempt to find // the next text sibling until it locates the text node in which the offset refers to // node: // The node to check. // offset: // The position to find within the text node // tags: // private. while(node.length < offset && node.nextSibling && node.nextSibling.nodeType==3){ //Adjust the offset and node in the case of multiple text nodes in a row offset = offset - node.length; node = node.nextSibling; } return {"node": node, "offset": offset}; }, removeTrailingBr: function(container){ // summary: // If last child of container is a
, then remove it. // tags: // private var para = /P|DIV|LI/i.test(container.tagName) ? container : selectionapi.getParentOfType(container,['P','DIV','LI']); if(!para){ return; } if(para.lastChild){ if((para.childNodes.length > 1 && para.lastChild.nodeType == 3 && /^[\s\xAD]*$/.test(para.lastChild.nodeValue)) || para.lastChild.tagName=='BR'){ domConstruct.destroy(para.lastChild); } } if(!para.childNodes.length){ para.innerHTML=this.bogusHtmlContent; } } }); });