eads@29: /** eads@29: * eads@29: * @author Sander Kruger eads@29: * @copyright Copyright © 2009, Sander Kruger, All rights reserved. eads@29: */ eads@29: eads@29: (function() { eads@29: var Event = tinymce.dom.Event; eads@29: var direction = 0; // Track cursor direction for skipping eads@29: var hasClicked = false; // Track mouseclicks for selecting eads@29: var atStart = null; // Cache if the cursor is at the start or at the end of eads@29: var atEnd = null; // an atom. eads@29: eads@29: tinymce.create('tinymce.plugins.AtomicPlugin', { eads@29: init : function(ed, url) { eads@29: var t = this, atomicClass; eads@29: eads@29: t.editor = ed; eads@29: atomicClass = ed.getParam("atomic_atomic_class", "mceAtomic"); eads@29: eads@29: ed.onNodeChange.addToTop(function(ed, cm, n) { eads@29: var sc, ec, wasEnd=false; eads@29: eads@29: // Check if the start or end of the selection is in an atom eads@29: sc = ed.dom.getParent(ed.selection.getStart(), function(n) { eads@29: return ed.dom.hasClass(n, atomicClass); eads@29: }); eads@29: eads@29: ec = ed.dom.getParent(ed.selection.getEnd(), function(n) { eads@29: return ed.dom.hasClass(n, atomicClass); eads@29: }); eads@29: if (atEnd) eads@29: { eads@29: wasEnd = true; eads@29: } eads@29: atStart = null; eads@29: atEnd = null; eads@29: // Check situation to move the cursor or selection. eads@29: if (sc || ec) eads@29: { eads@29: var s = ed.selection.getSel(); eads@29: var r = ed.selection.getRng(); eads@29: var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset); eads@29: var move = false; eads@29: if (!select) eads@29: { eads@29: // The user has not selected anything but is moving the eads@29: // cursor or clicked on an atom. eads@29: if (direction == -1) eads@29: { eads@29: if (r.setStart) eads@29: { eads@29: var lc = t._leftNeighbour( ed, sc ); eads@29: if (lc) eads@29: { eads@29: // Make sure that when skipping to the left, eads@29: // the next input doesn't go into the atom. eads@29: var offset = lc.nodeType==3?lc.length:1; eads@29: r.setStart( lc, offset ); eads@29: r.setEnd( lc, offset ); eads@29: } eads@29: else eads@29: { eads@29: r.setStart( sc, 0 ); eads@29: r.setEnd( sc, 0 ); eads@29: } eads@29: atStart = sc; eads@29: move = true; eads@29: } eads@29: else eads@29: { eads@29: // ie (check if the caret is not just after the eads@29: // atom) eads@29: var r2 = ed.getBody().createTextRange(); eads@29: r2.moveToElementText( ec ); eads@29: r2.collapse(false); eads@29: if (!r2.isEqual( r )) eads@29: { eads@29: r.moveToElementText( sc ); eads@29: r.collapse(); eads@29: atStart = sc; eads@29: move = true; eads@29: } eads@29: else eads@29: { eads@29: var tempEl = ed.dom.create("span", null, "­"); eads@29: ec.insertAdjacentElement("afterEnd", tempEl); eads@29: eads@29: r.moveToElementText(tempEl); eads@29: r.collapse(false); eads@29: ed.selection.setRng(r); eads@29: eads@29: ed.dom.remove(tempEl); eads@29: atEnd = ec; eads@29: } eads@29: } eads@29: } eads@29: else if (direction == 1) eads@29: { eads@29: if (r.setStartAfter) eads@29: { eads@29: r.setStartAfter( ec ); eads@29: r.setEndAfter( ec ); eads@29: move = true; eads@29: } eads@29: else eads@29: { eads@29: if (wasEnd) eads@29: { eads@29: r.move( 'character' ); eads@29: ed.selection.setRng(r); eads@29: ec = null; eads@29: } eads@29: else eads@29: { eads@29: var tempEl = ed.dom.create("span", null, "­"); eads@29: ec.insertAdjacentElement("afterEnd", tempEl); eads@29: eads@29: r.moveToElementText(tempEl); eads@29: r.collapse(false); eads@29: ed.selection.setRng(r); eads@29: eads@29: ed.dom.remove(tempEl); eads@29: } eads@29: } eads@29: atEnd = ec; eads@29: } eads@29: else if (hasClicked) eads@29: { eads@29: if (r.setStart) eads@29: { eads@29: r.setStart( sc, 0 ); eads@29: r.setEnd( ec, ec.childNodes.length ); eads@29: } eads@29: else eads@29: { eads@29: // ie eads@29: r.moveToElementText( sc ); eads@29: } eads@29: move = true; eads@29: } eads@29: else if (wasEnd && !r.setStart) eads@29: { eads@29: // ie eads@29: var tempEl = ed.dom.create("span", null, "­"); eads@29: ec.insertAdjacentElement("afterEnd", tempEl); eads@29: eads@29: r.moveToElementText(tempEl); eads@29: r.collapse(false); eads@29: ed.selection.setRng(r); eads@29: eads@29: ed.dom.remove(tempEl); eads@29: atEnd = ec; eads@29: } eads@29: } eads@29: else eads@29: { eads@29: // Reverse selections have the anchor at the end and the eads@29: // focus at the start. This happens when selecting eads@29: // backwards. Make sure the extended selection keeps the eads@29: // same direction. eads@29: // *** Issue: ie doesn't support backward selections eads@29: // programmatically (or at least I have not found how to eads@29: // do this). eads@29: var reverse = false; eads@29: if (s.anchorNode && s.anchorNode == r.endContainer && s.anchorOffset == r.endOffset) eads@29: { eads@29: reverse = true; eads@29: } eads@29: var moveStart = false; eads@29: var moveEnd = false; eads@29: var rsc, rec; eads@29: if (sc) eads@29: { eads@29: if (s.anchorNode) eads@29: { eads@29: if (sc != r.startContainer || (r.startOffset != 0 && r.startOffset != sc.childNodes.length)) eads@29: { eads@29: // The start pos is not where it should be. eads@29: moveStart = true; eads@29: } eads@29: } eads@29: else eads@29: { eads@29: // ie eads@29: rsc = ed.getBody().createTextRange(); eads@29: rsc.moveToElementText( sc ); eads@29: moveStart = (r.compareEndPoints( "StartToStart", rsc ) != 0); eads@29: } eads@29: } eads@29: if (ec) eads@29: { eads@29: if (s.anchorNode) eads@29: { eads@29: if (ec != r.endContainer || (r.endOffset != 0 && r.endOffset != ec.childNodes.length)) eads@29: { eads@29: // The end pos is not where it should be eads@29: moveEnd = true; eads@29: } eads@29: } eads@29: else eads@29: { eads@29: // ie eads@29: rec = ed.getBody().createTextRange(); eads@29: rec.moveToElementText( ec ); eads@29: moveEnd = (r.compareEndPoints( "EndToEnd", rec ) != 0); eads@29: } eads@29: } eads@29: if (reverse) eads@29: { eads@29: // Start with the end. Since 'selection direction detection' eads@29: // doesn't work on IE, don't bother with the IE code here. eads@29: if (moveEnd) eads@29: { eads@29: // If both start and end move and they move leftwards, set start first and then extend instead of starting with setend eads@29: if (direction == -1) eads@29: { eads@29: r.setEnd( ec, 0 ); eads@29: } eads@29: else eads@29: { eads@29: r.setEnd( ec, ec.childNodes.length ); eads@29: } eads@29: move = true; eads@29: } eads@29: if (moveStart) eads@29: { eads@29: // Use the extend method to advance the focus eads@29: // position of the selection. eads@29: if (direction == 1) eads@29: { eads@29: s.extend( sc, sc.childNodes.length ); eads@29: move = false; eads@29: } eads@29: else eads@29: { eads@29: s.extend( sc, 0 ); eads@29: move = false; eads@29: } eads@29: } eads@29: } eads@29: else eads@29: { eads@29: if (moveStart) eads@29: { eads@29: if (direction == 1) eads@29: { eads@29: if (r.setStart) eads@29: { eads@29: r.setStart( sc, sc.childNodes.length ); eads@29: } eads@29: else eads@29: { eads@29: // ie eads@29: r.setEndPoint( "StartToEnd", rsc ); eads@29: } eads@29: } eads@29: else eads@29: { eads@29: if (r.setStart) eads@29: { eads@29: r.setStart( sc, 0 ); eads@29: } eads@29: else eads@29: { eads@29: // ie eads@29: r.setEndPoint( "StartToStart", rsc ); eads@29: } eads@29: } eads@29: move = true; eads@29: } eads@29: if (moveEnd) eads@29: { eads@29: if (direction == -1) eads@29: { eads@29: if (r.setEnd) eads@29: { eads@29: s.extend( ec, 0 ); eads@29: } eads@29: else eads@29: { eads@29: // ie eads@29: r.setEndPoint( "EndToStart", rec ); eads@29: move = true; eads@29: } eads@29: } eads@29: else eads@29: { eads@29: if (r.setEnd) eads@29: { eads@29: s.extend( ec, ec.childNodes.length ); eads@29: } eads@29: else eads@29: { eads@29: // ie eads@29: r.setEndPoint( "EndToEnd", rec ); eads@29: move = true; eads@29: } eads@29: } eads@29: // move = false; eads@29: } eads@29: } eads@29: } eads@29: if (move) eads@29: { eads@29: // Make sure other modules can react to this event. eads@29: ed.selection.setRng( r ); eads@29: } eads@29: } eads@29: }); eads@29: eads@29: ed.onKeyDown.addToTop( function(ed, e) { eads@29: var k = e.keyCode, atomicClass; eads@29: atomicClass = ed.getParam("atomic_atomic_class", "mceAtomic"); eads@29: hasClicked = false; eads@29: direction = 0; eads@29: if (k == 37 || k == 38) eads@29: { eads@29: direction = -1; eads@29: } eads@29: else if (k == 39 || k == 40) eads@29: { eads@29: direction = 1; eads@29: } eads@29: else if (k == 8) eads@29: { eads@29: // Check if an atom is being 'backspace'd eads@29: if (!atEnd) eads@29: { eads@29: var s = ed.selection.getSel(); eads@29: var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset); eads@29: if (!select) eads@29: { eads@29: var ep = s.focusNode?((s.focusNode.nodeType == 3 && s.focusOffset != 0)?null:s.focusNode):ed.selection.getStart(); eads@29: if (ed.dom.hasClass(ep, atomicClass)) eads@29: { eads@29: atEnd = ep; eads@29: } eads@29: else if (ep) eads@29: { eads@29: var rc; eads@29: if (!s.focusNode) eads@29: { eads@29: // ie eads@29: // focusOffset is not given. So, we need to find eads@29: // it by walking through the children and comparing eads@29: // end points. eads@29: var r = ed.selection.getRng(); eads@29: var r2 = ed.getBody().createTextRange(); eads@29: r2.moveToElementText( ep ); eads@29: if (r2.compareEndPoints( "StartToStart", r ) == 0) eads@29: { eads@29: // The caret is at the start of this element. eads@29: // Find the left neighbouring element eads@29: rc = t._leftNeighbour( ed, ep ); eads@29: } eads@29: else eads@29: { eads@29: // The focus is not at the start of the parent. eads@29: // Find the child left of the caret. eads@29: rc = ep.firstChild; eads@29: while (rc) eads@29: { eads@29: // We are not interested in text nodes eads@29: // because they cannot be atoms (all by eads@29: // themselves) eads@29: if (rc.nodeType == 1) eads@29: { eads@29: r2.moveToElementText( rc ); eads@29: if (r.compareEndPoints( "EndToEnd", r2 ) == 0) eads@29: { eads@29: break; eads@29: } eads@29: } eads@29: rc = rc.nextSibling; eads@29: } eads@29: } eads@29: } eads@29: else eads@29: { eads@29: if (ep.nodeType == 1 && s.focusOffset != 0) eads@29: { eads@29: // ep encloses the current caret position. eads@29: // select the child to the left of the caret. eads@29: rc = ep.childNodes[s.focusOffset-1]; eads@29: } eads@29: else eads@29: { eads@29: // The caret is on the left-most end of ep. eads@29: // Find it's left neighbour. eads@29: rc = t._leftNeighbour( ed, ep ); eads@29: } eads@29: } eads@29: if (rc) eads@29: { eads@29: // Now see if this element has a right-most eads@29: // descendent that is an atom eads@29: while (rc && !ed.dom.hasClass(rc, atomicClass)) eads@29: { eads@29: rc = rc.lastChild; eads@29: } eads@29: if (rc) eads@29: { eads@29: atEnd = rc; eads@29: } eads@29: } eads@29: } eads@29: } eads@29: } eads@29: if (atEnd) eads@29: { eads@29: ed.dom.remove( atEnd ); eads@29: atEnd = null; eads@29: return Event.cancel(e); eads@29: } eads@29: } eads@29: else if (k == 46) eads@29: { eads@29: // Check if an atom is being 'delete'd eads@29: if (!atStart) eads@29: { eads@29: var s = ed.selection.getSel(); eads@29: var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset); eads@29: if (!select) eads@29: { eads@29: var ep = s.focusNode?((s.focusNode.nodeType == 3 && s.focusOffset != s.focusNode.length)?null:s.focusNode):ed.selection.getEnd(); eads@29: if (ed.dom.hasClass(ep, atomicClass)) eads@29: { eads@29: atStart = ep; eads@29: } eads@29: else if (ep) eads@29: { eads@29: var lc; eads@29: if (!s.focusNode) eads@29: { eads@29: // ie eads@29: // focusOffset is not given. So, we need to find eads@29: // it by walking through the children and comparing eads@29: // end points. eads@29: var r = ed.selection.getRng(); eads@29: var r2 = ed.getBody().createTextRange(); eads@29: r2.moveToElementText( ep ); eads@29: if (r2.compareEndPoints( "EndToEnd", r ) == 0) eads@29: { eads@29: // The caret is at the start of this element. eads@29: // Find the right neighbouring element eads@29: lc = t._rightNeighbour( ed, ep ); eads@29: } eads@29: else eads@29: { eads@29: // The focus is not at the end of the parent. eads@29: // Find the child right of the caret. eads@29: lc = ep.firstChild; eads@29: while (lc) eads@29: { eads@29: // We are not interested in text nodes eads@29: // because they cannot be atoms (all by eads@29: // themselves) eads@29: if (lc.nodeType == 1) eads@29: { eads@29: r2.moveToElementText( lc ); eads@29: if (r.compareEndPoints( "StartToStart", r2 ) == 0) eads@29: { eads@29: break; eads@29: } eads@29: } eads@29: lc = lc.nextSibling; eads@29: } eads@29: } eads@29: } eads@29: else eads@29: { eads@29: if (ep.nodeType == 1 && s.focusOffset != ep.childNodes.length) eads@29: { eads@29: // ep encloses the current caret position. eads@29: // select the child to the right of the caret. eads@29: lc = ep.childNodes[s.focusOffset]; eads@29: } eads@29: else eads@29: { eads@29: // The caret is on the right-most end of ep. eads@29: // Find it's right neighbour. eads@29: lc = t._rightNeighbour( ed, ep ); eads@29: } eads@29: } eads@29: if (lc) eads@29: { eads@29: // Now see if this element has a left-most eads@29: // descendent that is an atom eads@29: while (lc && !ed.dom.hasClass(lc, atomicClass)) eads@29: { eads@29: lc = lc.firstChild; eads@29: } eads@29: if (lc) eads@29: { eads@29: atStart = lc; eads@29: } eads@29: } eads@29: } eads@29: } eads@29: } eads@29: if (atStart) eads@29: { eads@29: ed.dom.remove( atStart ); eads@29: atStart = null; eads@29: return Event.cancel(e); eads@29: } eads@29: } eads@29: }); eads@29: eads@29: ed.onMouseDown.addToTop(t._onClick); eads@29: ed.onMouseUp.addToTop(t._onClick); eads@29: }, eads@29: eads@29: getInfo : function() { eads@29: return { eads@29: longname : 'Atomic elements', eads@29: author : 'Sander Kruger', eads@29: authorurl : 'http://www.3gsp.eu', eads@29: infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/atomic', eads@29: version : tinymce.majorVersion + "." + tinymce.minorVersion eads@29: }; eads@29: }, eads@29: eads@29: _onClick : function(ed, e) { eads@29: direction = 0; eads@29: hasClicked = true; eads@29: atStart = null; eads@29: atEnd = null; eads@29: }, eads@29: eads@29: // Helper methods eads@29: _leftNeighbour : function( ed, e ) { eads@29: var l = ed.dom.getParent(e, function(n) { eads@29: return e.previousSibling != null; eads@29: }); eads@29: if (l) eads@29: { eads@29: return l.previousSibling; eads@29: } eads@29: return null; eads@29: }, eads@29: eads@29: _rightNeighbour : function( ed, e ) { eads@29: var r = ed.dom.getParent(e, function(n) { eads@29: return e.nextSibling != null; eads@29: }); eads@29: if (r) eads@29: { eads@29: return r.nextSibling; eads@29: } eads@29: return null; eads@29: } eads@29: }); eads@29: eads@29: // Register plugin eads@29: tinymce.PluginManager.add('atomic', tinymce.plugins.AtomicPlugin); eads@29: })();