diff options
Diffstat (limited to 'lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js')
-rw-r--r-- | lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js | 622 |
1 files changed, 622 insertions, 0 deletions
diff --git a/lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js b/lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js new file mode 100644 index 000000000..e5c7b7466 --- /dev/null +++ b/lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js @@ -0,0 +1,622 @@ +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/sniff", // has("ie") has("mozilla") has("webkit") + "dojo/_base/window", // win.withGlobal + "dojo/window", // winUtils.scrollIntoView + "../_Plugin", + "../RichText", + "../range", + "../../_base/focus" +], function(declare, domConstruct, event, keys, lang, has, win, winUtils, _Plugin, RichText, rangeapi, baseFocus){ + +// module: +// dijit/_editor/plugins/EnterKeyHandling + +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 <shift-ENTER> + // | second line of first paragraph <ENTER> + // | second paragraph + // + // will generate: + // + // | <p> + // | first paragraph + // | <br/> + // | second line of first paragraph + // | </p> + // | <p> + // | second paragraph + // | </p> + // + // 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 <ENTER> + // | two <ENTER> + // | three <ENTER> + // | <ENTER> + // | four <ENTER> + // | five <ENTER> + // | six <ENTER> + // + // It will appear on the screen as two 'paragraphs' of three lines each. Markupwise, this generates: + // + // BR: + // | one<br/> + // | two<br/> + // | three<br/> + // | <br/> + // | four<br/> + // | five<br/> + // | six<br/> + // + // DIV: + // | <div>one</div> + // | <div>two</div> + // | <div>three</div> + // | <div> </div> + // | <div>four</div> + // | <div>five</div> + // | <div>six</div> + + // 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 <br>, 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', baseFocus)){ + var liparent = this.editor._sCall('getAncestorElement', ['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 = this.editor._sCall('getAncestorElement', [this.blockNodeForEnter]); + if(block){ + block.innerHTML=this.bogusHtmlContent; + if(has("ie") <= 9){ + // 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 <br> + var parent = this.editor._sCall('getParentElement', []); + 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; + 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(this.editor.window); + 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. + 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(this.editor.window); + newrange.setStart(endNode,0); + newrange.setEnd(endNode, endNode.length); + selection.removeAllRanges(); + selection.addRange(newrange); + if(endEmpty && !has("webkit")){ + this.editor._sCall("remove", []); + }else{ + this.editor._sCall("collapse", [true]); + } + }else{ + var targetNode; + if(range.startOffset >= 0){ + targetNode = rs.childNodes[range.startOffset]; + } + 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(this.editor.window); + newrange.setStart(endNode,0); + newrange.setEnd(endNode, endNode.length); + selection.removeAllRanges(); + selection.addRange(newrange); + this.editor._sCall("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', '<br>'); + } + } + 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 <br/>, 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|<span\b[^>]*\bclass=['"]Apple-style-span['"][^>]*>(\s| | |\xA0)<\/span>)?(<br>)?$/.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: this.editor._sCall('getAncestorElement', [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 <br/>, 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 <br/>, 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 `<br>`, then remove it. + // tags: + // private + var para = /P|DIV|LI/i.test(container.tagName) ? + container : this.editor._sCall("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; + } + } +}); + +}); |