4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
13 var Event = tinymce.dom.Event,
15 isGecko = tinymce.isGecko,
16 isOpera = tinymce.isOpera,
18 extend = tinymce.extend,
22 function cloneFormats(node) {
23 var clone, temp, inner;
26 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {
28 temp = node.cloneNode(false);
29 temp.appendChild(clone);
32 clone = inner = node.cloneNode(false);
35 clone.removeAttribute('id');
37 } while (node = node.parentNode);
40 return {wrapper : clone, inner : inner};
43 // Checks if the selection/caret is at the end of the specified block element
44 function isAtEnd(rng, par) {
45 var rng2 = par.ownerDocument.createRange();
47 rng2.setStart(rng.endContainer, rng.endOffset);
48 rng2.setEndAfter(par);
50 // Get number of characters to the right of the cursor if it's zero then we are at the end and need to merge the next block element
51 return rng2.cloneContents().textContent.length == 0;
54 function splitList(selection, dom, li) {
57 if (dom.isEmpty(li)) {
58 listBlock = dom.getParent(li, 'ul,ol');
60 if (!dom.getParent(listBlock.parentNode, 'ul,ol')) {
61 dom.split(listBlock, li);
62 block = dom.create('p', 0, '<br data-mce-bogus="1" />');
63 dom.replace(block, li);
64 selection.select(block, 1);
74 * This is a internal class and no method in this class should be called directly form the out side.
76 tinymce.create('tinymce.ForceBlocks', {
77 ForceBlocks : function(ed) {
78 var t = this, s = ed.settings, elm;
82 elm = (s.forced_root_block || 'p').toLowerCase();
83 s.element = elm.toUpperCase();
85 ed.onPreInit.add(t.setup, t);
87 if (s.forced_root_block) {
88 ed.onInit.add(t.forceRoots, t);
89 ed.onSetContent.add(t.forceRoots, t);
90 ed.onBeforeGetContent.add(t.forceRoots, t);
91 ed.onExecCommand.add(function(ed, cmd) {
92 if (cmd == 'mceInsertContent') {
101 var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection;
103 // Force root blocks when typing and when getting output
104 if (s.forced_root_block) {
105 ed.onBeforeExecCommand.add(t.forceRoots, t);
106 ed.onKeyUp.add(t.forceRoots, t);
107 ed.onPreProcess.add(t.forceRoots, t);
110 if (s.force_br_newlines) {
111 // Force IE to produce BRs on enter
113 ed.onKeyPress.add(function(ed, e) {
116 if (e.keyCode == 13 && selection.getNode().nodeName != 'LI') {
117 selection.setContent('<br id="__" /> ', {format : 'raw'});
119 n.removeAttribute('id');
121 selection.collapse();
122 return Event.cancel(e);
128 if (s.force_p_newlines) {
130 ed.onKeyPress.add(function(ed, e) {
131 if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e))
135 // Ungly hack to for IE to preserve the formatting when you press
136 // enter at the end of a block element with formatted contents
137 // This logic overrides the browsers default logic with
138 // custom logic that enables us to control the output
139 tinymce.addUnload(function() {
140 t._previousFormats = 0; // Fix IE leak
143 ed.onKeyPress.add(function(ed, e) {
144 t._previousFormats = 0;
146 // Clone the current formats, this will later be applied to the new block contents
147 if (e.keyCode == 13 && !e.shiftKey && ed.selection.isCollapsed() && s.keep_styles)
148 t._previousFormats = cloneFormats(ed.selection.getStart());
151 ed.onKeyUp.add(function(ed, e) {
152 // Let IE break the element and the wrap the new caret location in the previous formats
153 if (e.keyCode == 13 && !e.shiftKey) {
154 var parent = ed.selection.getStart(), fmt = t._previousFormats;
156 // Parent is an empty block
157 if (!parent.hasChildNodes() && fmt) {
158 parent = dom.getParent(parent, dom.isBlock);
160 if (parent && parent.nodeName != 'LI') {
161 parent.innerHTML = '';
163 if (t._previousFormats) {
164 parent.appendChild(fmt.wrapper);
165 fmt.inner.innerHTML = '\uFEFF';
167 parent.innerHTML = '\uFEFF';
169 selection.select(parent, 1);
170 selection.collapse(true);
171 ed.getDoc().execCommand('Delete', false, null);
172 t._previousFormats = 0;
180 ed.onKeyDown.add(function(ed, e) {
181 if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey)
182 t.backspaceDelete(e, e.keyCode == 8);
187 // Workaround for missing shift+enter support, http://bugs.webkit.org/show_bug.cgi?id=16973
188 if (tinymce.isWebKit) {
189 function insertBr(ed) {
190 var rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h;
193 rng.insertNode(br = dom.create('br'));
195 // Place caret after BR
196 rng.setStartAfter(br);
198 selection.setRng(rng);
200 // Could not place caret after BR then insert an nbsp entity and move the caret
201 if (selection.getSel().focusNode == br.previousSibling) {
202 selection.select(dom.insertAfter(dom.doc.createTextNode('\u00a0'), br));
203 selection.collapse(TRUE);
206 // Create a temporary DIV after the BR and get the position as it
207 // seems like getPos() returns 0 for text nodes and BR elements.
208 dom.insertAfter(div, br);
209 divYPos = dom.getPos(div).y;
212 // Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117
213 if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port.
214 ed.getWin().scrollTo(0, divYPos);
217 ed.onKeyPress.add(function(ed, e) {
218 if (e.keyCode == 13 && (e.shiftKey || (s.force_br_newlines && !dom.getParent(selection.getNode(), 'h1,h2,h3,h4,h5,h6,ol,ul')))) {
227 // Replaces IE:s auto generated paragraphs with the specified element name
228 if (s.element != 'P') {
229 ed.onKeyPress.add(function(ed, e) {
230 t.lastElm = selection.getNode().nodeName;
233 ed.onKeyUp.add(function(ed, e) {
234 var bl, n = selection.getNode(), b = ed.getBody();
236 if (b.childNodes.length === 1 && n.nodeName == 'P') {
237 n = dom.rename(n, s.element);
239 selection.collapse();
241 } else if (e.keyCode == 13 && !e.shiftKey && t.lastElm != 'P') {
242 bl = dom.getParent(n, 'p');
245 dom.rename(bl, s.element);
254 find : function(n, t, s) {
255 var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, FALSE), c = -1;
257 while (n = w.nextNode()) {
261 if (t == 0 && n == s)
265 if (t == 1 && c == s)
272 forceRoots : function(ed, e) {
273 var t = this, ed = t.editor, b = ed.getBody(), d = ed.getDoc(), se = ed.selection, s = se.getSel(), r = se.getRng(), si = -2, ei, so, eo, tr, c = -0xFFFFFF;
274 var nx, bl, bp, sp, le, nl = b.childNodes, i, n, eid;
276 // Fix for bug #1863847
277 //if (e && e.keyCode == 13)
280 // Wrap non blocks into blocks
281 for (i = nl.length - 1; i >= 0; i--) {
284 // Ignore internal elements
285 if (nx.nodeType === 1 && nx.getAttribute('data-mce-type')) {
290 // Is text or non block element
291 if (nx.nodeType === 3 || (!t.dom.isBlock(nx) && nx.nodeType !== 8 && !/^(script|mce:script|style|mce:style)$/i.test(nx.nodeName))) {
293 // Create new block but ignore whitespace
294 if (nx.nodeType != 3 || /[^\s]/g.test(nx.nodeValue)) {
297 if (!isIE || r.setStart) {
298 // If selection is element then mark it
299 if (r.startContainer.nodeType == 1 && (n = r.startContainer.childNodes[r.startOffset]) && n.nodeType == 1) {
300 // Save the id of the selected element
301 eid = n.getAttribute("id");
302 n.setAttribute("id", "__mce");
304 // If element is inside body, might not be the case in contentEdiable mode
305 if (ed.dom.getParent(r.startContainer, function(e) {return e === b;})) {
308 si = t.find(b, 0, r.startContainer);
309 ei = t.find(b, 0, r.endContainer);
313 // Force control range into text range
315 tr = d.body.createTextRange();
316 tr.moveToElementText(r.item(0));
320 tr = d.body.createTextRange();
321 tr.moveToElementText(b);
323 bp = tr.move('character', c) * -1;
327 sp = tr.move('character', c) * -1;
331 le = (tr.move('character', c) * -1) - sp;
338 // Uses replaceChild instead of cloneNode since it removes selected attribute from option elements on IE
339 // See: http://support.microsoft.com/kb/829907
340 bl = ed.dom.create(ed.settings.forced_root_block);
341 nx.parentNode.replaceChild(bl, nx);
345 if (bl.hasChildNodes())
346 bl.insertBefore(nx, bl.firstChild);
351 bl = null; // Time to create new block
356 if (!isIE || r.setStart) {
357 bl = b.getElementsByTagName(ed.settings.element)[0];
360 // Select last location or generated block
362 r.setStart(t.find(b, 1, si), so);
366 // Select last location or generated block
368 r.setEnd(t.find(b, 1, ei), eo);
379 r.moveToElementText(b);
381 r.moveStart('character', si);
382 r.moveEnd('character', ei);
388 } else if ((!isIE || r.setStart) && (n = ed.dom.get('__mce'))) {
389 // Restore the id of the selected element
391 n.setAttribute('id', eid);
393 n.removeAttribute('id');
395 // Move caret before selected element
403 getParentBlock : function(n) {
406 return d.getParent(n, d.isBlock);
409 insertPara : function(e) {
410 var t = this, ed = t.editor, dom = ed.dom, d = ed.getDoc(), se = ed.settings, s = ed.selection.getSel(), r = s.getRangeAt(0), b = d.body;
411 var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car;
413 ed.undoManager.beforeChange();
415 // If root blocks are forced then use Operas default behavior since it's really good
416 // Removed due to bug: #1853816
417 // if (se.forced_root_block && isOpera)
420 // Setup before range
421 rb = d.createRange();
423 // If is before the first block element and in body, then move it into first block element
424 rb.setStart(s.anchorNode, s.anchorOffset);
428 ra = d.createRange();
430 // If is before the first block element and in body, then move it into first block element
431 ra.setStart(s.focusNode, s.focusOffset);
434 // Setup start/end points
435 dir = rb.compareBoundaryPoints(rb.START_TO_END, ra) < 0;
436 sn = dir ? s.anchorNode : s.focusNode;
437 so = dir ? s.anchorOffset : s.focusOffset;
438 en = dir ? s.focusNode : s.anchorNode;
439 eo = dir ? s.focusOffset : s.anchorOffset;
441 // If selection is in empty table cell
442 if (sn === en && /^(TD|TH)$/.test(sn.nodeName)) {
443 if (sn.firstChild.nodeName == 'BR')
444 dom.remove(sn.firstChild); // Remove BR
446 // Create two new block elements
447 if (sn.childNodes.length == 0) {
448 ed.dom.add(sn, se.element, null, '<br />');
449 aft = ed.dom.add(sn, se.element, null, '<br />');
453 ed.dom.add(sn, se.element, null, n);
454 aft = ed.dom.add(sn, se.element, null, '<br />');
457 // Move caret into the last one
459 r.selectNodeContents(aft);
461 ed.selection.setRng(r);
466 // If the caret is in an invalid location in FF we need to move it into the first block
467 if (sn == b && en == b && b.firstChild && ed.dom.isBlock(b.firstChild)) {
468 sn = en = sn.firstChild;
470 rb = d.createRange();
472 ra = d.createRange();
476 // Never use body as start or end node
477 sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
478 sn = sn.nodeName == "BODY" ? sn.firstChild : sn;
479 en = en.nodeName == "HTML" ? d.body : en; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
480 en = en.nodeName == "BODY" ? en.firstChild : en;
482 // Get start and end blocks
483 sb = t.getParentBlock(sn);
484 eb = t.getParentBlock(en);
485 bn = sb ? sb.nodeName : se.element; // Get block name to create
487 // Return inside list use default browser behavior
488 if (n = t.dom.getParent(sb, 'li,pre')) {
489 if (n.nodeName == 'LI')
490 return splitList(ed.selection, t.dom, n);
495 // If caption or absolute layers then always generate new blocks within
496 if (sb && (sb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
501 // If caption or absolute layers then always generate new blocks within
502 if (eb && (eb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
508 if (/(TD|TABLE|TH|CAPTION)/.test(bn) || (sb && bn == "DIV" && /left|right/gi.test(dom.getStyle(sb, 'float', 1)))) {
513 // Setup new before and after blocks
514 bef = (sb && sb.nodeName == bn) ? sb.cloneNode(0) : ed.dom.create(bn);
515 aft = (eb && eb.nodeName == bn) ? eb.cloneNode(0) : ed.dom.create(bn);
517 // Remove id from after clone
518 aft.removeAttribute('id');
520 // Is header and cursor is at the end, then force paragraph under
521 if (/^(H[1-6])$/.test(bn) && isAtEnd(r, sb))
522 aft = ed.dom.create(se.element);
524 // Find start chop node
527 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
531 } while ((n = n.previousSibling ? n.previousSibling : n.parentNode));
533 // Find end chop node
536 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
540 } while ((n = n.nextSibling ? n.nextSibling : n.parentNode));
542 // Place first chop part into before block element
543 if (sc.nodeName == bn)
546 rb.setStartBefore(sc);
549 bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
551 // Place secnd chop part within new block element
555 //console.debug(s.focusNode, s.focusOffset);
559 aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
561 // Create range around everything
563 if (!sc.previousSibling && sc.parentNode.nodeName == bn) {
564 r.setStartBefore(sc.parentNode);
566 if (rb.startContainer.nodeName == bn && rb.startOffset == 0)
567 r.setStartBefore(rb.startContainer);
569 r.setStart(rb.startContainer, rb.startOffset);
572 if (!ec.nextSibling && ec.parentNode.nodeName == bn)
573 r.setEndAfter(ec.parentNode);
575 r.setEnd(ra.endContainer, ra.endOffset);
577 // Delete and replace it with new block elements
581 ed.getWin().scrollTo(0, vp.y);
583 // Never wrap blocks in blocks
584 if (bef.firstChild && bef.firstChild.nodeName == bn)
585 bef.innerHTML = bef.firstChild.innerHTML;
587 if (aft.firstChild && aft.firstChild.nodeName == bn)
588 aft.innerHTML = aft.firstChild.innerHTML;
591 if (dom.isEmpty(bef))
592 bef.innerHTML = '<br />';
594 function appendStyles(e, en) {
595 var nl = [], nn, n, i;
599 // Make clones of style elements
600 if (se.keep_styles) {
603 // We only want style specific elements
604 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(n.nodeName)) {
605 nn = n.cloneNode(FALSE);
606 dom.setAttrib(nn, 'id', ''); // Remove ID since it needs to be unique
609 } while (n = n.parentNode);
612 // Append style elements to aft
614 for (i = nl.length - 1, nn = e; i >= 0; i--)
615 nn = nn.appendChild(nl[i]);
617 // Padd most inner style element
618 nl[0].innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
619 return nl[0]; // Move caret to most inner element
621 e.innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
624 // Fill empty afterblook with current style
625 if (dom.isEmpty(aft))
626 car = appendStyles(aft, en);
628 // Opera needs this one backwards for older versions
629 if (isOpera && parseFloat(opera.version()) < 9.5) {
642 return d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE).nextNode() || n;
645 // Move cursor and scroll into view
647 r.selectNodeContents(isGecko ? first(car || aft) : car || aft);
652 // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs
653 y = ed.dom.getPos(aft).y;
654 //ch = aft.clientHeight;
656 // Is element within viewport
657 if (y < vp.y || y + 25 > vp.y + vp.h) {
658 ed.getWin().scrollTo(0, y < vp.y ? y : y - vp.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks
661 'Element: y=' + y + ', h=' + ch + ', ' +
662 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h)
666 ed.undoManager.add();
671 backspaceDelete : function(e, bs) {
672 var t = this, ed = t.editor, b = ed.getBody(), dom = ed.dom, n, se = ed.selection, r = se.getRng(), sc = r.startContainer, n, w, tn, walker;
674 // Delete when caret is behind a element doesn't work correctly on Gecko see #3011651
675 if (!bs && r.collapsed && sc.nodeType == 1 && r.startOffset == sc.childNodes.length) {
676 walker = new tinymce.dom.TreeWalker(sc.lastChild, sc);
678 // Walk the dom backwards until we find a text node
679 for (n = sc.lastChild; n; n = walker.prev()) {
680 if (n.nodeType == 3) {
681 r.setStart(n, n.nodeValue.length);
689 // The caret sometimes gets stuck in Gecko if you delete empty paragraphs
690 // This workaround removes the element by hand and moves the caret to the previous element
691 if (sc && ed.dom.isBlock(sc) && !/^(TD|TH)$/.test(sc.nodeName) && bs) {
692 if (sc.childNodes.length == 0 || (sc.childNodes.length == 1 && sc.firstChild.nodeName == 'BR')) {
693 // Find previous block element
695 while ((n = n.previousSibling) && !ed.dom.isBlock(n)) ;
698 if (sc != b.firstChild) {
699 // Find last text node
700 w = ed.dom.doc.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE);
701 while (tn = w.nextNode())
704 // Place caret at the end of last text node
705 r = ed.getDoc().createRange();
706 r.setStart(n, n.nodeValue ? n.nodeValue.length : 0);
707 r.setEnd(n, n.nodeValue ? n.nodeValue.length : 0);
710 // Remove the target container
714 return Event.cancel(e);