/*
* Based on the tinyMCE core paragraph handling
* Released under LGPL License.
*
* License: http://tinymce.moxiecode.com/license
* Contributing: http://tinymce.moxiecode.com/contributing
*/
(function() {
tinymce.create('tinymce.plugins.annoParagraphs', {
init : function(ed, url) {
var t = this;
t.editor = ed;
ed.onKeyDown.addToTop(function(ed, e) {
// If we're not hitting the shift key, we are hitting the return key, and we're not in a list (retain new list item functionality)
if (!e.shiftKey && e.keyCode == 13) {
if (!t.insertPara(e)) {
e.preventDefault();
ed.undoManager.add();
}
t._nodeChanged(ed);
return false;
}
return true;
});
// Disable tab for everything except lists.
ed.onKeyUp.addToTop(function(ed, e) {
if (e.keyCode == 9 && ed.dom.getParent(ed.selection.getNode(), 'LIST-ITEM') == null) {
e.preventDefault();
return false;
}
return preventDefaultKey(ed, e);
});
function preventDefaultKey(ed, e) {
var parent = ed.dom.getParent(ed.selection.getNode(), 'LIST, LIST-ITEM');
if (!e.shiftKey && e.keyCode == 13 && !parent) {
e.preventDefault();
return false;
}
return true;
}
ed.onKeyPress.addToTop(function(ed, e) {
return preventDefaultKey(ed, e);
});
},
// Create a controlManager to hangle new nodes, the dispatchers for onKeyUp, onKeyPress etc.. does not get passed a CM
createControl : function (ed, cm) {
this.cm = cm;
},
// A new node is selected programtically, not be user. onNodeChange won't work, we need to add it to keypress.
_nodeChanged : function (ed) {
if (c = this.cm.get('annoformatselect')) {
var parent = ed.dom.getParent(ed.selection.getNode(), 'HEADING, PARA, SEC'), selVal;
if (parent) {
selVal = parent.nodeName.toLowerCase();
}
else {
selVal = 'format';
}
c.select(selVal);
}
},
getParentBlock : function(n) {
var t = this, ed = t.editor;
return ed.dom.getParent(n, ed.dom.isBlock);
},
insertPara : function(e) {
var t = this, ed = t.editor, dom = ed.dom, d = ed.getDoc(), se = ed.settings, s = ed.selection.getSel(), r = ed.selection.getRng(), b = d.body;
var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car;
var TRUE = true, FALSE = false, newElement, node = ed.selection.getNode();
ed.undoManager.beforeChange();
// Override default tinyMCE element.
se.element = 'para';
if (e.ctrlKey || /(BODY|HTML|HEADING|SEC)/.test(node.nodeName.toUpperCase())) {
function insertNewBlock(node) {
var newElement, parentNode;
if (dom.getParent(node, 'PARA') !== null) {
node = dom.getParent(node, 'PARA');
}
// If we're not in a section already, or this node is a section, insert a new section block
if ((!(parentNode = dom.getParent(node, 'SEC')) || node.nodeName.toUpperCase() == 'SEC') && e.ctrlKey) {
newElement = newSec();
}
else {
newElement = dom.create('PARA');
}
// If we're not trying to insert a new section and we're in a section node, just return insert a paragraph at the cursor
if (node.nodeName.toUpperCase() == 'SEC' && !e.ctrlKey) {
// Inefficient mechanism to insert node at selection then select it, but tinyMCE offers no other method currently
// Set an ID so it can be searched for later
dom.setAttribs(newElement, { id: '_anno_inserted' });
// Inser the node into at the current selection - note this does not return the dom node, just the node passed into it
ed.selection.setNode(newElement);
// Find the newly inserted node by ID
newElement = dom.get('_anno_inserted');
// Remove ID, so this process can run again
dom.setAttribs(newElement, null);
return newElement;
}
else {
return dom.insertAfter(newElement, node);
}
}
// Create a new sec element with a title
function newSec() {
var sec = dom.create('sec', null);
dom.add(sec, 'heading', null, ' ');
dom.add(sec, 'para');
return sec;
}
// Just insert a new paragraph if the ctrl key isn't held and the carat is in a para tag
// Or, various tags should create paragraphs, not enter a br (when the ctrl key is held).
if (/(DISP-FORMULA|TABLE-WRAP|FIG|DISP-QUOTE|HEADING)/.test(node.nodeName.toUpperCase())) {
newElement = insertNewBlock(node);
}
else if (/(BODY|HTML)/.test(node.nodeName.toUpperCase())) {
secElement = dom.add(node, 'sec');
newElement = dom.add(secElement, 'heading', null, ' ');
dom.add(secElement, 'para');
}
else if (parentNode = dom.getParent(node, 'FIG')) {
newElement = insertNewBlock(parentNode);
}
else if (parentNode = dom.getParent(node, 'TABLE-WRAP')) {
newElement = insertNewBlock(parentNode);
}
else if (parentNode = dom.getParent(node, 'SEC')) {
newElement = insertNewBlock(parentNode);
}
else {
newElement = insertNewBlock(node);
}
// Set new element as the first title tag, so we can select it
if (newElement.nodeName.toLowerCase() == 'sec') {
var eleArray = dom.select(' > heading', newElement);
if (eleArray.length > 0) {
newElement = eleArray[0];
}
}
// Move caret to the freshly created item
if (d.createRange) { // all browsers, except IE before version 9
r = d.createRange();
r.selectNodeContents(newElement);
}
else { // IE < 9
r = d.selection.createRange();
r.moveToElementText(newElement);
}
r.collapse(1);
ed.selection.setRng(r);
ed.undoManager.add();
return FALSE;
}
// Setup before range
rb = d.createRange();
// If is before the first block element and in body, then move it into first block element
rb.setStart(s.anchorNode, s.anchorOffset);
rb.collapse(TRUE);
// Setup after range
ra = d.createRange();
// If is before the first block element and in body, then move it into first block element
ra.setStart(s.focusNode, s.focusOffset);
ra.collapse(TRUE);
// Setup start/end points
dir = rb.compareBoundaryPoints(rb.START_TO_END, ra) < 0;
sn = dir ? s.anchorNode : s.focusNode;
so = dir ? s.anchorOffset : s.focusOffset;
en = dir ? s.focusNode : s.anchorNode;
eo = dir ? s.focusOffset : s.anchorOffset;
// If selection is in empty table cell
if (sn === en && /^(TD|TH|CAP)$/.test(sn.nodeName.toUpperCase())) {
if (sn.firstChild && sn.firstChild.nodeName.toLowerCase() == 'br')
dom.remove(sn.firstChild); // Remove BR
// Create two new block elements
if (sn.childNodes.length == 0) {
ed.dom.add(sn, se.element, null, '
');
aft = ed.dom.add(sn, se.element, null, '
');
} else {
n = sn.innerHTML;
sn.innerHTML = '';
ed.dom.add(sn, se.element, null, n);
aft = ed.dom.add(sn, se.element, null, '
');
}
// Move caret into the last one
r = d.createRange();
r.selectNodeContents(aft);
r.collapse(1);
ed.selection.setRng(r);
ed.undoManager.add();
return FALSE;
}
function insertBr(ed) {
var selection = ed.selection, rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h;
// Insert BR element
rng.insertNode(br = dom.create('br'));
// Place caret after BR
rng.setStartAfter(br);
rng.setEndAfter(br);
selection.setRng(rng);
// Could not place caret after BR then insert an nbsp entity and move the caret
if (selection.getSel().focusNode == br.previousSibling) {
selection.select(dom.insertAfter(dom.doc.createTextNode('\u00a0'), br));
selection.collapse(TRUE);
}
// Create a temporary DIV after the BR and get the position as it
// seems like getPos() returns 0 for text nodes and BR elements.
dom.insertAfter(div, br);
divYPos = dom.getPos(div).y;
dom.remove(div);
// Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117
if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port.
ed.getWin().scrollTo(0, divYPos);
};
// If the caret is in an invalid location in FF we need to move it into the first block
if (sn == b && en == b && b.firstChild && ed.dom.isBlock(b.firstChild)) {
sn = en = sn.firstChild;
so = eo = 0;
rb = d.createRange();
rb.setStart(sn, 0);
ra = d.createRange();
ra.setStart(en, 0);
}
// Never use body as start or end node
sn = sn.nodeName.toUpperCase() == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
sn = sn.nodeName.toUpperCase() == "BODY" ? sn.firstChild : sn;
en = en.nodeName.toUpperCase() == "HTML" ? d.body : en; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
en = en.nodeName.toUpperCase() == "BODY" ? en.firstChild : en;
// Get start and end blocks
sb = t.getParentBlock(sn);
eb = t.getParentBlock(en);
bn = sb ? sb.nodeName : se.element; // Get block name to create
// Return inside list use default browser behavior
if (n = dom.getParent(sb, 'list-item,pre')) {
if (n.nodeName.toUpperCase() == 'LIST-ITEM') {
return annoListBreak(ed.selection, dom, n);
}
ed.undoManager.add();
return TRUE;
}
// If the list item is empty, break out of it
function annoListBreak(selection, dom, li) {
var listBlock, block;
if (dom.isEmpty(li) || li.innerHTML == '
') {
listBlock = dom.getParent(li, 'list');
if (!dom.getParent(listBlock.parentNode, 'list')) {
dom.split(listBlock, li);
}
ed.undoManager.add();
return FALSE;
}
ed.undoManager.add();
return TRUE;
};
if (!/^(PARA|BODY|HTML)$/.test(bn.toUpperCase())) {
insertBr(ed);
ed.undoManager.add();
return FALSE;
}
// If caption or absolute layers then always generate new blocks within
if (sb && (sb.nodeName.toUpperCase() == 'CAP' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
bn = se.element;
sb = null;
}
// Use P instead
if (/(TD|TABLE|TH|CAP)/.test(bn.toUpperCase()) || (sb && bn.toUpperCase() == "DIV" && /left|right/gi.test(dom.getStyle(sb, 'float', 1)))) {
bn = se.element;
sb = eb = null;
}
// Setup new before and after blocks
bef = (sb && sb.nodeName.toUpperCase() == bn) ? sb.cloneNode(0) : ed.dom.create(bn);
aft = (eb && eb.nodeName.toUpperCase() == bn) ? eb.cloneNode(0) : ed.dom.create(bn);
// Remove id from after clone
aft.removeAttribute('id');
// Is header and cursor is at the end, then force paragraph under
if (/^(HEADING)$/.test(bn) && isAtEnd(r, sb))
aft = ed.dom.create(se.element);
// Find start chop node
n = sc = sn;
do {
if (n == b || n.nodeType == 9 || dom.isBlock(n) || /(TD|TABLE|TH|CAP)/.test(n.nodeName))
break;
sc = n;
} while ((n = n.previousSibling ? n.previousSibling : n.parentNode));
// Find end chop node
n = ec = en;
do {
if (n == b || n.nodeType == 9 || dom.isBlock(n) || /(TD|TABLE|TH|CAP)/.test(n.nodeName))
break;
ec = n;
} while ((n = n.nextSibling ? n.nextSibling : n.parentNode));
// Place first chop part into before block element
if (sc.nodeName.toUpperCase() == bn)
rb.setStart(sc, 0);
else
rb.setStartBefore(sc);
rb.setEnd(sn, so);
bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
// Place secnd chop part within new block element
try {
ra.setEndAfter(ec);
} catch(ex) {
//console.debug(s.focusNode, s.focusOffset);
}
ra.setStart(en, eo);
aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
// Create range around everything
r = d.createRange();
if (!sc.previousSibling && sc.parentNode.nodeName.toUpperCase() == bn) {
r.setStartBefore(sc.parentNode);
} else {
if (rb.startContainer.nodeName.toUpperCase() == bn && rb.startOffset == 0)
r.setStartBefore(rb.startContainer);
else
r.setStart(rb.startContainer, rb.startOffset);
}
if (!ec.nextSibling && ec.parentNode.nodeName.toUpperCase() == bn)
r.setEndAfter(ec.parentNode);
else
r.setEnd(ra.endContainer, ra.endOffset);
// Delete and replace it with new block elements
r.deleteContents();
// Never wrap blocks in blocks
if (bef.firstChild && bef.firstChild.nodeName.toUpperCase() == bn)
bef.innerHTML = bef.firstChild.innerHTML;
if (aft.firstChild && aft.firstChild.nodeName.toUpperCase() == bn)
aft.innerHTML = aft.firstChild.innerHTML;
function appendStyles(e, en) {
var nl = [], nn, n, i;
e.innerHTML = '';
// Make clones of style elements
if (se.keep_styles) {
n = en;
do {
// We only want style specific elements
if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(n.nodeName.toUpperCase())) {
nn = n.cloneNode(FALSE);
dom.setAttrib(nn, 'id', ''); // Remove ID since it needs to be unique
nl.push(nn);
}
} while (n = n.parentNode);
}
// Append style elements to aft
if (nl.length > 0) {
for (i = nl.length - 1, nn = e; i >= 0; i--)
nn = nn.appendChild(nl[i]);
// Padd most inner style element
nl[0].innerHTML = isOpera ? '\u00a0' : '
'; // Extra space for Opera so that the caret can move there
return nl[0]; // Move caret to most inner element
} else
e.innerHTML = '';
// e.innerHTML = isOpera ? '\u00a0' : '
'; // Extra space for Opera so that the caret can move there
};
// Padd empty blocks
if (dom.isEmpty(bef))
appendStyles(bef, sn);
// Fill empty afterblook with current style
if (dom.isEmpty(aft))
car = appendStyles(aft, en);
// Opera needs this one backwards for older versions
if (tinymce.isOpera && parseFloat(opera.version()) < 9.5) {
r.insertNode(bef);
r.insertNode(aft);
}
else {
r.insertNode(aft);
r.insertNode(bef);
}
aft.normalize();
bef.normalize();
if (!aft.innerHTML) {// && !tinymce.isIE) {
aft.innerHTML = '
';
}
// Move cursor and scroll into view
ed.selection.select(aft, true);
ed.selection.collapse(true);
if ((aft.innerHTML == '
' || aft.innerHTML == '
') && tinymce.isIE) {
aft.innerHTML = '';
}
// 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
y = ed.dom.getPos(aft).y;
//ch = aft.clientHeight;
// Is element within viewport
if (y < vp.y || y + 25 > vp.y + vp.h) {
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
}
ed.undoManager.add();
return FALSE;
},
getInfo : function() {
return {
longname : 'Annotum Paragraphs',
author : 'Crowd Favorite',
authorurl : 'http://crowdfavorite.com',
infourl : '',
version : "1.0"
};
}
});
// Register plugin
tinymce.PluginManager.add('annoParagraphs', tinymce.plugins.annoParagraphs);
})();