4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
12 * Contains logic for handling the enter key to split/generate block elements.
14 define("tinymce/EnterKey", [
15 "tinymce/dom/TreeWalker",
17 ], function(TreeWalker, Env) {
18 var isIE = Env.ie && Env.ie < 11;
20 return function(editor) {
21 var dom = editor.dom, selection = editor.selection, settings = editor.settings;
22 var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements();
24 function handleEnterKey(evt) {
25 var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey,
26 newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer;
28 // Returns true if the block can be split into two blocks or not
29 function canSplitBlock(node) {
32 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
33 !/^(fixed|absolute)/i.test(node.style.position) &&
34 dom.getContentEditable(node) !== "true";
37 // Renders empty block on IE
38 function renderBlockOnIE(block) {
41 if (dom.isBlock(block)) {
42 oldRng = selection.getRng();
43 block.appendChild(dom.create('span', null, '\u00a0'));
44 selection.select(block);
45 block.lastChild.outerHTML = '';
46 selection.setRng(oldRng);
50 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
51 function trimInlineElementsOnLeftSideOfBlock(block) {
52 var node = block, firstChilds = [], i;
54 // Find inner most first child ex: <p><i><b>*</b></i></p>
55 while ((node = node.firstChild)) {
56 if (dom.isBlock(node)) {
60 if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
61 firstChilds.push(node);
65 i = firstChilds.length;
67 node = firstChilds[i];
68 if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) {
71 // Remove <a> </a> see #5381
72 if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') {
79 // Moves the caret to a suitable position within the root for example in the first non
80 // pure whitespace text node or before an image
81 function moveToCaretPosition(root) {
82 var walker, node, rng, lastNode = root, tempElm;
84 rng = dom.createRng();
86 if (root.hasChildNodes()) {
87 walker = new TreeWalker(root, root);
89 while ((node = walker.current())) {
90 if (node.nodeType == 3) {
91 rng.setStart(node, 0);
96 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
97 rng.setStartBefore(node);
98 rng.setEndBefore(node);
103 node = walker.next();
107 rng.setStart(lastNode, 0);
108 rng.setEnd(lastNode, 0);
111 if (root.nodeName == 'BR') {
112 if (root.nextSibling && dom.isBlock(root.nextSibling)) {
113 // Trick on older IE versions to render the caret before the BR between two lists
114 if (!documentMode || documentMode < 9) {
115 tempElm = dom.create('br');
116 root.parentNode.insertBefore(tempElm, root);
119 rng.setStartBefore(root);
120 rng.setEndBefore(root);
122 rng.setStartAfter(root);
123 rng.setEndAfter(root);
126 rng.setStart(root, 0);
131 selection.setRng(rng);
133 // Remove tempElm created for old IE:s
135 selection.scrollIntoView(root);
138 // Creates a new block element by cloning the current one or creating a new one if the name is specified
139 // This function will also copy any text formatting from the parent block and add it to the new one
140 function createNewBlock(name) {
141 var node = container, block, clonedNode, caretNode;
143 block = name || parentBlockName == "TABLE" ? dom.create(name || newBlockName) : parentBlock.cloneNode(false);
146 // Clone any parent styles
147 if (settings.keep_styles !== false) {
149 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {
150 // Never clone a caret containers
151 if (node.id == '_mce_caret') {
155 clonedNode = node.cloneNode(false);
156 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique
158 if (block.hasChildNodes()) {
159 clonedNode.appendChild(block.firstChild);
160 block.appendChild(clonedNode);
162 caretNode = clonedNode;
163 block.appendChild(clonedNode);
166 } while ((node = node.parentNode));
169 // BR is needed in empty blocks on non IE browsers
171 caretNode.innerHTML = '<br data-mce-bogus="1">';
177 // Returns true/false if the caret is at the start/end of the parent block element
178 function isCaretAtStartOrEndOfBlock(start) {
179 var walker, node, name;
181 // Caret is in the middle of a text node like "a|b"
182 if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) {
186 // If after the last element in block node edge case for #5091
187 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) {
191 // If the caret if before the first element in parentBlock
192 if (start && container.nodeType == 1 && container == parentBlock.firstChild) {
196 // Caret can be before/after a table
197 if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) {
198 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);
201 // Walk the DOM and look for text nodes or non empty elements
202 walker = new TreeWalker(container, parentBlock);
204 // If caret is in beginning or end of a text block then jump to the next/previous node
205 if (container.nodeType == 3) {
206 if (start && offset === 0) {
208 } else if (!start && offset == container.nodeValue.length) {
213 while ((node = walker.current())) {
214 if (node.nodeType === 1) {
215 // Ignore bogus elements
216 if (!node.getAttribute('data-mce-bogus')) {
217 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>
218 name = node.nodeName.toLowerCase();
219 if (nonEmptyElementsMap[name] && name !== 'br') {
223 } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) {
237 // Wraps any text nodes or inline elements in the specified forced root block name
238 function wrapSelfAndSiblingsInDefaultBlock(container, offset) {
239 var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P';
241 // Not in a block element or in a table cell or caption
242 parentBlock = dom.getParent(container, dom.isBlock);
243 rootBlockName = editor.getBody().nodeName.toLowerCase();
244 if (!parentBlock || !canSplitBlock(parentBlock)) {
245 parentBlock = parentBlock || editableRoot;
247 if (!parentBlock.hasChildNodes()) {
248 newBlock = dom.create(blockName);
249 parentBlock.appendChild(newBlock);
250 rng.setStart(newBlock, 0);
251 rng.setEnd(newBlock, 0);
255 // Find parent that is the first child of parentBlock
257 while (node.parentNode != parentBlock) {
258 node = node.parentNode;
261 // Loop left to find start node start wrapping at
262 while (node && !dom.isBlock(node)) {
264 node = node.previousSibling;
267 if (startNode && schema.isValidChild(rootBlockName, blockName.toLowerCase())) {
268 newBlock = dom.create(blockName);
269 startNode.parentNode.insertBefore(newBlock, startNode);
271 // Start wrapping until we hit a block
273 while (node && !dom.isBlock(node)) {
274 next = node.nextSibling;
275 newBlock.appendChild(node);
279 // Restore range to it's past location
280 rng.setStart(container, offset);
281 rng.setEnd(container, offset);
288 // Inserts a block or br before/after or in the middle of a split list of the LI is empty
289 function handleEmptyListItem() {
290 function isFirstOrLastLi(first) {
291 var node = containerBlock[first ? 'firstChild' : 'lastChild'];
293 // Find first/last element since there might be whitespace there
295 if (node.nodeType == 1) {
299 node = node[first ? 'nextSibling' : 'previousSibling'];
302 return node === parentBlock;
305 function getContainerBlock() {
306 var containerBlockParent = containerBlock.parentNode;
308 if (containerBlockParent.nodeName == 'LI') {
309 return containerBlockParent;
312 return containerBlock;
315 // Check if we are in an nested list
316 var containerBlockParentName = containerBlock.parentNode.nodeName;
317 if (/^(OL|UL|LI)$/.test(containerBlockParentName)) {
321 newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR');
323 if (isFirstOrLastLi(true) && isFirstOrLastLi()) {
324 if (containerBlockParentName == 'LI') {
325 // Nested list is inside a LI
326 dom.insertAfter(newBlock, getContainerBlock());
328 // Is first and last list item then replace the OL/UL with a text block
329 dom.replace(newBlock, containerBlock);
331 } else if (isFirstOrLastLi(true)) {
332 if (containerBlockParentName == 'LI') {
333 // List nested in an LI then move the list to a new sibling LI
334 dom.insertAfter(newBlock, getContainerBlock());
335 newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed
336 newBlock.appendChild(containerBlock);
338 // First LI in list then remove LI and add text block before list
339 containerBlock.parentNode.insertBefore(newBlock, containerBlock);
341 } else if (isFirstOrLastLi()) {
342 // Last LI in list then remove LI and add text block after list
343 dom.insertAfter(newBlock, getContainerBlock());
344 renderBlockOnIE(newBlock);
346 // Middle LI in list the split the list and insert a text block in the middle
347 // Extract after fragment and insert it after the current block
348 containerBlock = getContainerBlock();
349 tmpRng = rng.cloneRange();
350 tmpRng.setStartAfter(parentBlock);
351 tmpRng.setEndAfter(containerBlock);
352 fragment = tmpRng.extractContents();
353 dom.insertAfter(fragment, containerBlock);
354 dom.insertAfter(newBlock, containerBlock);
357 dom.remove(parentBlock);
358 moveToCaretPosition(newBlock);
362 // Walks the parent block to the right and look for BR elements
363 function hasRightSideContent() {
364 var walker = new TreeWalker(container, parentBlock), node;
366 while ((node = walker.next())) {
367 if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) {
373 // Inserts a BR element if the forced_root_block option is set to false or empty string
374 function insertBr() {
375 var brElm, extraBr, marker;
377 if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
378 // Insert extra BR element at the end block elements
379 if (!isIE && !hasRightSideContent()) {
380 brElm = dom.create('br');
381 rng.insertNode(brElm);
382 rng.setStartAfter(brElm);
383 rng.setEndAfter(brElm);
388 brElm = dom.create('br');
389 rng.insertNode(brElm);
391 // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it
392 if (isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) {
393 brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);
396 // Insert temp marker and scroll to that
397 marker = dom.create('span', {}, ' ');
398 brElm.parentNode.insertBefore(marker, brElm);
399 selection.scrollIntoView(marker);
403 rng.setStartAfter(brElm);
404 rng.setEndAfter(brElm);
406 rng.setStartBefore(brElm);
407 rng.setEndBefore(brElm);
410 selection.setRng(rng);
414 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
415 function trimLeadingLineBreaks(node) {
417 if (node.nodeType === 3) {
418 node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
421 node = node.firstChild;
425 function getEditableRoot(node) {
426 var root = dom.getRoot(), parent, editableRoot;
428 // Get all parents until we hit a non editable parent or the root
430 while (parent !== root && dom.getContentEditable(parent) !== "false") {
431 if (dom.getContentEditable(parent) === "true") {
432 editableRoot = parent;
435 parent = parent.parentNode;
438 return parent !== root ? editableRoot : root;
441 // Adds a BR at the end of blocks that only contains an IMG or INPUT since
442 // these might be floated and then they won't expand the block
443 function addBrToBlockIfNeeded(block) {
446 // IE will render the blocks correctly other browsers needs a BR
448 block.normalize(); // Remove empty text nodes that got left behind by the extract
450 // Check if the block is empty or contains a floated last child
451 lastChild = block.lastChild;
452 if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {
453 dom.add(block, 'br');
458 // Delete any selected contents
459 if (!rng.collapsed) {
460 editor.execCommand('Delete');
464 // Event is blocked by some other handler for example the lists plugin
465 if (evt.isDefaultPrevented()) {
469 // Setup range items and newBlockName
470 container = rng.startContainer;
471 offset = rng.startOffset;
472 newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;
473 newBlockName = newBlockName ? newBlockName.toUpperCase() : '';
474 documentMode = dom.doc.documentMode;
475 shiftKey = evt.shiftKey;
477 // Resolve node index
478 if (container.nodeType == 1 && container.hasChildNodes()) {
479 isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
480 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
481 if (isAfterLastNodeInContainer && container.nodeType == 3) {
482 offset = container.nodeValue.length;
488 // Get editable root node normaly the body element but sometimes a div or span
489 editableRoot = getEditableRoot(container);
491 // If there is no editable root then enter is done inside a contentEditable false element
496 undoManager.beforeChange();
498 // If editable root isn't block nor the root of the editor
499 if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {
500 if (!newBlockName || shiftKey) {
507 // Wrap the current node and it's sibling in a default block if it's needed.
508 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
509 // This won't happen if root blocks are disabled or the shiftKey is pressed
510 if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {
511 container = wrapSelfAndSiblingsInDefaultBlock(container, offset);
514 // Find parent block and setup empty block paddings
515 parentBlock = dom.getParent(container, dom.isBlock);
516 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
519 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
520 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
522 // Enter inside block contained within a LI then split or insert before/after LI
523 if (containerBlockName == 'LI' && !evt.ctrlKey) {
524 parentBlock = containerBlock;
525 parentBlockName = containerBlockName;
528 // Handle enter in LI
529 if (parentBlockName == 'LI') {
530 if (!newBlockName && shiftKey) {
535 // Handle enter inside an empty list item
536 if (dom.isEmpty(parentBlock)) {
537 handleEmptyListItem();
542 // Don't split PRE tags but insert a BR instead easier when writing code samples etc
543 if (parentBlockName == 'PRE' && settings.br_in_pre !== false) {
549 // If no root block is configured then insert a BR by default or if the shiftKey is pressed
550 if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) {
556 // If parent block is root then never insert new blocks
557 if (newBlockName && parentBlock === editor.getBody()) {
561 // Default block name if it's not configured
562 newBlockName = newBlockName || 'P';
564 // Insert new block before/after the parent block depending on caret location
565 if (isCaretAtStartOrEndOfBlock()) {
566 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup
567 if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') {
568 newBlock = createNewBlock(newBlockName);
570 newBlock = createNewBlock();
573 // Split the current container block element if enter is pressed inside an empty inner block element
574 if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) {
575 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P
576 newBlock = dom.split(containerBlock, parentBlock);
578 dom.insertAfter(newBlock, parentBlock);
581 moveToCaretPosition(newBlock);
582 } else if (isCaretAtStartOrEndOfBlock(true)) {
583 // Insert new block before
584 newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);
585 renderBlockOnIE(newBlock);
587 // Extract after fragment and insert it after the current block
588 tmpRng = rng.cloneRange();
589 tmpRng.setEndAfter(parentBlock);
590 fragment = tmpRng.extractContents();
591 trimLeadingLineBreaks(fragment);
592 newBlock = fragment.firstChild;
593 dom.insertAfter(fragment, parentBlock);
594 trimInlineElementsOnLeftSideOfBlock(newBlock);
595 addBrToBlockIfNeeded(parentBlock);
596 moveToCaretPosition(newBlock);
599 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique
603 editor.on('keydown', function(evt) {
604 if (evt.keyCode == 13) {
605 if (handleEnterKey(evt) !== false) {
606 evt.preventDefault();