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);
89 var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection, blockElements = ed.schema.getBlockElements();
92 if (s.forced_root_block) {
93 function addRootBlocks() {
94 var node = selection.getStart(), rootNode = ed.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF;
96 if (!node || node.nodeType !== 1)
99 // Check if node is wrapped in block
100 while (node != rootNode) {
101 if (blockElements[node.nodeName])
104 node = node.parentNode;
107 // Get current selection
108 rng = selection.getRng();
110 startContainer = rng.startContainer;
111 startOffset = rng.startOffset;
112 endContainer = rng.endContainer;
113 endOffset = rng.endOffset;
115 // Force control range into text range
117 rng = ed.getDoc().body.createTextRange();
118 rng.moveToElementText(rng.item(0));
121 tmpRng = rng.duplicate();
122 tmpRng.collapse(true);
123 startOffset = tmpRng.move('character', offset) * -1;
125 if (!tmpRng.collapsed) {
126 tmpRng = rng.duplicate();
127 tmpRng.collapse(false);
128 endOffset = (tmpRng.move('character', offset) * -1) - startOffset;
132 // Wrap non block elements and text nodes
133 for (node = rootNode.firstChild; node; node) {
134 if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) {
135 if (!rootBlockNode) {
136 rootBlockNode = dom.create(s.forced_root_block);
137 node.parentNode.insertBefore(rootBlockNode, node);
141 node = node.nextSibling;
142 rootBlockNode.appendChild(tempNode);
144 rootBlockNode = null;
145 node = node.nextSibling;
150 rng.setStart(startContainer, startOffset);
151 rng.setEnd(endContainer, endOffset);
152 selection.setRng(rng);
155 rng = ed.getDoc().body.createTextRange();
156 rng.moveToElementText(rootNode);
158 rng.moveStart('character', startOffset);
161 rng.moveEnd('character', endOffset);
172 ed.onKeyUp.add(addRootBlocks);
173 ed.onClick.add(addRootBlocks);
176 if (s.force_br_newlines) {
177 // Force IE to produce BRs on enter
179 ed.onKeyPress.add(function(ed, e) {
182 if (e.keyCode == 13 && selection.getNode().nodeName != 'LI') {
183 selection.setContent('<br id="__" /> ', {format : 'raw'});
185 n.removeAttribute('id');
187 selection.collapse();
188 return Event.cancel(e);
194 if (s.force_p_newlines) {
196 ed.onKeyPress.add(function(ed, e) {
197 if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e))
201 // Ungly hack to for IE to preserve the formatting when you press
202 // enter at the end of a block element with formatted contents
203 // This logic overrides the browsers default logic with
204 // custom logic that enables us to control the output
205 tinymce.addUnload(function() {
206 t._previousFormats = 0; // Fix IE leak
209 ed.onKeyPress.add(function(ed, e) {
210 t._previousFormats = 0;
212 // Clone the current formats, this will later be applied to the new block contents
213 if (e.keyCode == 13 && !e.shiftKey && ed.selection.isCollapsed() && s.keep_styles)
214 t._previousFormats = cloneFormats(ed.selection.getStart());
217 ed.onKeyUp.add(function(ed, e) {
218 // Let IE break the element and the wrap the new caret location in the previous formats
219 if (e.keyCode == 13 && !e.shiftKey) {
220 var parent = ed.selection.getStart(), fmt = t._previousFormats;
222 // Parent is an empty block
223 if (!parent.hasChildNodes() && fmt) {
224 parent = dom.getParent(parent, dom.isBlock);
226 if (parent && parent.nodeName != 'LI') {
227 parent.innerHTML = '';
229 if (t._previousFormats) {
230 parent.appendChild(fmt.wrapper);
231 fmt.inner.innerHTML = '\uFEFF';
233 parent.innerHTML = '\uFEFF';
235 selection.select(parent, 1);
236 selection.collapse(true);
237 ed.getDoc().execCommand('Delete', false, null);
238 t._previousFormats = 0;
246 ed.onKeyDown.add(function(ed, e) {
247 if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey)
248 t.backspaceDelete(e, e.keyCode == 8);
253 // Workaround for missing shift+enter support, http://bugs.webkit.org/show_bug.cgi?id=16973
254 if (tinymce.isWebKit) {
255 function insertBr(ed) {
256 var rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h;
259 rng.insertNode(br = dom.create('br'));
261 // Place caret after BR
262 rng.setStartAfter(br);
264 selection.setRng(rng);
266 // Could not place caret after BR then insert an nbsp entity and move the caret
267 if (selection.getSel().focusNode == br.previousSibling) {
268 selection.select(dom.insertAfter(dom.doc.createTextNode('\u00a0'), br));
269 selection.collapse(TRUE);
272 // Create a temporary DIV after the BR and get the position as it
273 // seems like getPos() returns 0 for text nodes and BR elements.
274 dom.insertAfter(div, br);
275 divYPos = dom.getPos(div).y;
278 // Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117
279 if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port.
280 ed.getWin().scrollTo(0, divYPos);
283 ed.onKeyPress.add(function(ed, e) {
284 if (e.keyCode == 13 && (e.shiftKey || (s.force_br_newlines && !dom.getParent(selection.getNode(), 'h1,h2,h3,h4,h5,h6,ol,ul')))) {
293 // Replaces IE:s auto generated paragraphs with the specified element name
294 if (s.element != 'P') {
295 ed.onKeyPress.add(function(ed, e) {
296 t.lastElm = selection.getNode().nodeName;
299 ed.onKeyUp.add(function(ed, e) {
300 var bl, n = selection.getNode(), b = ed.getBody();
302 if (b.childNodes.length === 1 && n.nodeName == 'P') {
303 n = dom.rename(n, s.element);
305 selection.collapse();
307 } else if (e.keyCode == 13 && !e.shiftKey && t.lastElm != 'P') {
308 bl = dom.getParent(n, 'p');
311 dom.rename(bl, s.element);
320 getParentBlock : function(n) {
323 return d.getParent(n, d.isBlock);
326 insertPara : function(e) {
327 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;
328 var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car;
330 ed.undoManager.beforeChange();
332 // If root blocks are forced then use Operas default behavior since it's really good
333 // Removed due to bug: #1853816
334 // if (se.forced_root_block && isOpera)
337 // Setup before range
338 rb = d.createRange();
340 // If is before the first block element and in body, then move it into first block element
341 rb.setStart(s.anchorNode, s.anchorOffset);
345 ra = d.createRange();
347 // If is before the first block element and in body, then move it into first block element
348 ra.setStart(s.focusNode, s.focusOffset);
351 // Setup start/end points
352 dir = rb.compareBoundaryPoints(rb.START_TO_END, ra) < 0;
353 sn = dir ? s.anchorNode : s.focusNode;
354 so = dir ? s.anchorOffset : s.focusOffset;
355 en = dir ? s.focusNode : s.anchorNode;
356 eo = dir ? s.focusOffset : s.anchorOffset;
358 // If selection is in empty table cell
359 if (sn === en && /^(TD|TH)$/.test(sn.nodeName)) {
360 if (sn.firstChild.nodeName == 'BR')
361 dom.remove(sn.firstChild); // Remove BR
363 // Create two new block elements
364 if (sn.childNodes.length == 0) {
365 ed.dom.add(sn, se.element, null, '<br />');
366 aft = ed.dom.add(sn, se.element, null, '<br />');
370 ed.dom.add(sn, se.element, null, n);
371 aft = ed.dom.add(sn, se.element, null, '<br />');
374 // Move caret into the last one
376 r.selectNodeContents(aft);
378 ed.selection.setRng(r);
383 // If the caret is in an invalid location in FF we need to move it into the first block
384 if (sn == b && en == b && b.firstChild && ed.dom.isBlock(b.firstChild)) {
385 sn = en = sn.firstChild;
387 rb = d.createRange();
389 ra = d.createRange();
393 // Never use body as start or end node
394 sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
395 sn = sn.nodeName == "BODY" ? sn.firstChild : sn;
396 en = en.nodeName == "HTML" ? d.body : en; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
397 en = en.nodeName == "BODY" ? en.firstChild : en;
399 // Get start and end blocks
400 sb = t.getParentBlock(sn);
401 eb = t.getParentBlock(en);
402 bn = sb ? sb.nodeName : se.element; // Get block name to create
404 // Return inside list use default browser behavior
405 if (n = t.dom.getParent(sb, 'li,pre')) {
406 if (n.nodeName == 'LI')
407 return splitList(ed.selection, t.dom, n);
412 // If caption or absolute layers then always generate new blocks within
413 if (sb && (sb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
418 // If caption or absolute layers then always generate new blocks within
419 if (eb && (eb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
425 if (/(TD|TABLE|TH|CAPTION)/.test(bn) || (sb && bn == "DIV" && /left|right/gi.test(dom.getStyle(sb, 'float', 1)))) {
430 // Setup new before and after blocks
431 bef = (sb && sb.nodeName == bn) ? sb.cloneNode(0) : ed.dom.create(bn);
432 aft = (eb && eb.nodeName == bn) ? eb.cloneNode(0) : ed.dom.create(bn);
434 // Remove id from after clone
435 aft.removeAttribute('id');
437 // Is header and cursor is at the end, then force paragraph under
438 if (/^(H[1-6])$/.test(bn) && isAtEnd(r, sb))
439 aft = ed.dom.create(se.element);
441 // Find start chop node
444 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
448 } while ((n = n.previousSibling ? n.previousSibling : n.parentNode));
450 // Find end chop node
453 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
457 } while ((n = n.nextSibling ? n.nextSibling : n.parentNode));
459 // Place first chop part into before block element
460 if (sc.nodeName == bn)
463 rb.setStartBefore(sc);
466 bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
468 // Place secnd chop part within new block element
472 //console.debug(s.focusNode, s.focusOffset);
476 aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
478 // Create range around everything
480 if (!sc.previousSibling && sc.parentNode.nodeName == bn) {
481 r.setStartBefore(sc.parentNode);
483 if (rb.startContainer.nodeName == bn && rb.startOffset == 0)
484 r.setStartBefore(rb.startContainer);
486 r.setStart(rb.startContainer, rb.startOffset);
489 if (!ec.nextSibling && ec.parentNode.nodeName == bn)
490 r.setEndAfter(ec.parentNode);
492 r.setEnd(ra.endContainer, ra.endOffset);
494 // Delete and replace it with new block elements
498 ed.getWin().scrollTo(0, vp.y);
500 // Never wrap blocks in blocks
501 if (bef.firstChild && bef.firstChild.nodeName == bn)
502 bef.innerHTML = bef.firstChild.innerHTML;
504 if (aft.firstChild && aft.firstChild.nodeName == bn)
505 aft.innerHTML = aft.firstChild.innerHTML;
507 function appendStyles(e, en) {
508 var nl = [], nn, n, i;
512 // Make clones of style elements
513 if (se.keep_styles) {
516 // We only want style specific elements
517 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(n.nodeName)) {
518 nn = n.cloneNode(FALSE);
519 dom.setAttrib(nn, 'id', ''); // Remove ID since it needs to be unique
522 } while (n = n.parentNode);
525 // Append style elements to aft
527 for (i = nl.length - 1, nn = e; i >= 0; i--)
528 nn = nn.appendChild(nl[i]);
530 // Padd most inner style element
531 nl[0].innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
532 return nl[0]; // Move caret to most inner element
534 e.innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
538 if (dom.isEmpty(bef))
539 appendStyles(bef, sn);
541 // Fill empty afterblook with current style
542 if (dom.isEmpty(aft))
543 car = appendStyles(aft, en);
545 // Opera needs this one backwards for older versions
546 if (isOpera && parseFloat(opera.version()) < 9.5) {
558 // Move cursor and scroll into view
559 ed.selection.select(aft, true);
560 ed.selection.collapse(true);
562 // 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
563 y = ed.dom.getPos(aft).y;
564 //ch = aft.clientHeight;
566 // Is element within viewport
567 if (y < vp.y || y + 25 > vp.y + vp.h) {
568 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
571 'Element: y=' + y + ', h=' + ch + ', ' +
572 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h)
576 ed.undoManager.add();
581 backspaceDelete : function(e, bs) {
582 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;
584 // Delete when caret is behind a element doesn't work correctly on Gecko see #3011651
585 if (!bs && r.collapsed && sc.nodeType == 1 && r.startOffset == sc.childNodes.length) {
586 walker = new tinymce.dom.TreeWalker(sc.lastChild, sc);
588 // Walk the dom backwards until we find a text node
589 for (n = sc.lastChild; n; n = walker.prev()) {
590 if (n.nodeType == 3) {
591 r.setStart(n, n.nodeValue.length);
599 // The caret sometimes gets stuck in Gecko if you delete empty paragraphs
600 // This workaround removes the element by hand and moves the caret to the previous element
601 if (sc && ed.dom.isBlock(sc) && !/^(TD|TH)$/.test(sc.nodeName) && bs) {
602 if (sc.childNodes.length == 0 || (sc.childNodes.length == 1 && sc.firstChild.nodeName == 'BR')) {
603 // Find previous block element
605 while ((n = n.previousSibling) && !ed.dom.isBlock(n)) ;
608 if (sc != b.firstChild) {
609 // Find last text node
610 w = ed.dom.doc.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE);
611 while (tn = w.nextNode())
614 // Place caret at the end of last text node
615 r = ed.getDoc().createRange();
616 r.setStart(n, n.nodeValue ? n.nodeValue.length : 0);
617 r.setEnd(n, n.nodeValue ? n.nodeValue.length : 0);
620 // Remove the target container
624 return Event.cancel(e);