summaryrefslogtreecommitdiff
path: root/lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js')
-rw-r--r--lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js622
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>&nbsp;</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: '&#160;', // &nbsp;
+
+ // 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|&nbsp;|&#160;|\xA0|<span\b[^>]*\bclass=['"]Apple-style-span['"][^>]*>(\s|&nbsp;|&#160;|\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;
+ }
+ }
+});
+
+});