dijit
Version:
Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible u
627 lines (594 loc) • 22.1 kB
JavaScript
define([
"dojo/_base/declare", // declare
"dojo/dom-construct", // domConstruct.destroy domConstruct.place
"dojo/keys", // keys.ENTER
"dojo/_base/lang",
"dojo/on",
"dojo/sniff", // has("ie") has("mozilla") has("webkit")
"dojo/_base/window", // win.withGlobal
"dojo/window", // winUtils.scrollIntoView
"../_Plugin",
"../RichText",
"../range"
], function(declare, domConstruct, keys, lang, on, has, win, winUtils, _Plugin, RichText, rangeapi){
// 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.own(on(editor.document, "keydown", lang.hitch(this, function(e){
if(e.keyCode == 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)){
e.stopPropagation();
e.preventDefault();
}
}
})));
if(has("ie") >= 9 && has("ie") <= 10){
this.own(on(editor.document, "paste", lang.hitch(this, function(e){
setTimeout(lang.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
var h = lang.hitch(this, "handleEnterKey");
editor.addKeyHandler(13, 0, 0, h); //enter
editor.addKeyHandler(13, 0, 1, h); //shift+enter
this.own(this.editor.on('KeyPressed', lang.hitch(this, 'onKeyPressed')));
}
},
onKeyPressed: function(){
// summary:
// Handler for after the user has pressed a key, and the display has been updated.
// Connected to RichText's onKeyPressed() method.
// tags:
// private
if(this._checkListLater){
if(this.editor.selection.isCollapsed()){
var liparent = this.editor.selection.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.selection.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.selection.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 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){
// Create dummy text with a   to go after the BR, to prevent IE crash.
// See https://bugs.dojotoolkit.org/ticket/12008 for details.
endNode = doc.createTextNode('\xA0');
}
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);
this.editor.selection.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.selection.collapse(true);
}
// \xA0 dummy text node remains, but is stripped before get("value")
// by RichText._stripTrailingEmptyNodes(). Still, could we just use a plain
// space (" ") instead?
}
}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.selection.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.selection.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;
}
}
});
});