app = function () { var _this = this, $document = $( document ); this.$editor = $('#editor'); this.$editor.bind( { 'focus': function( e ) { $document.unbind( '.surfaceView' ); $document.bind( { 'keydown.surfaceView': function( e ) { return _this.onKeyDown( e ); }, 'keyup.surfaceView': function( e ) { return _this.onKeyUp( e ); }, 'keypress.surfaceView': function( e ) { return _this.onKeyPress( e ); } } ); }, 'blur': function( e ) { $document.unbind( '.surfaceView' ); } } ); document.addEventListener( 'compositionstart', function( e ) { _this.onCompositionStart( e ); } ); document.addEventListener( 'compositionend', function( e ) { _this.onCompositionEnd( e ); } ); this.$editor.mousedown( function(e) { return _this.onMouseDown( e ); } ); // Set initial content for the "editor" // this.$editor.html("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.... New text..."); this.$editor.html("New book is good"); // this.$editor.html("... w"); this.$editor.addClass('leafNode'); this.prevText = app.getDOMText2(this.$editor[0]); this.prevHash = app.getDOMHash(this.$editor[0]); this.prevOffset = null; this.inIME = false; setInterval(function() { _this.loopFunc(); }, 100); }; app.prototype.onCompositionStart = function( e ) { console.log('inIME', true); this.inIME = true; }; app.prototype.onCompositionEnd = function( e ) { console.log('inIME', false); this.inIME = false; }; app.prototype.onKeyDown = function( e ) { }; app.prototype.onKeyUp = function( e ) { }; app.prototype.onKeyPress = function( e ) { }; app.prototype.onMouseDown = function( e ) { }; app.prototype.loopFunc = function() { var selection = rangy.getSelection(); if ( !selection.anchorNode || selection.anchorNode.nodeName === 'BODY' ) { return; } if ( this.inIME === true ) { return; } var text = app.getDOMText2( this.$editor[0] ), hash = app.getDOMHash( this.$editor[0] ), offset = ( selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset ) ? this.getOffset( selection.anchorNode, selection.anchorOffset ) : null; if ( text !== this.prevText ) { var lengthDiff = text.length - this.prevText.length, offsetDiff = offset - this.prevOffset; if ( lengthDiff === offsetDiff && offset !== null && this.prevOffset !== null ) { console.log("new text", text.substring( this.prevOffset, offset ), this.prevOffset); } else { var sameFromLeft = 0, sameFromRight = 0, l = Math.max( this.prevText.length, text.length ); while ( sameFromLeft < l && this.prevText[sameFromLeft] == text[sameFromLeft] ) { ++sameFromLeft; } l = l - sameFromLeft; while ( sameFromRight < l && this.prevText[this.prevText.length - 1 - sameFromRight] == text[text.length - 1 - sameFromRight] ) { ++sameFromRight; } console.log('to delete', this.prevText.substring( sameFromLeft, this.prevText.length - sameFromRight), sameFromLeft ); console.log('to insert', text.substring( sameFromLeft, text.length - sameFromRight ), sameFromLeft ); } this.prevText = text; } if ( hash !== this.prevHash ) { this.prevHash = hash; } this.prevOffset = offset; }; app.getDOMText2 = function( elem ) { var regex = new RegExp("[" + String.fromCharCode(32) + String.fromCharCode(160) + "]", "g"); return app.getDOMText( elem ).replace( regex, " " ); }; app.getDOMText = function( elem ) { var nodeType = elem.nodeType, ret = ''; if ( nodeType === 1 || nodeType === 9 ) { // Use textContent || innerText for elements if ( typeof elem.textContent === 'string' ) { return elem.textContent; } else if ( typeof elem.innerText === 'string' ) { // Replace IE's carriage returns return elem.innerText.replace( /\r\n/g, '' ); } else { // Traverse it's children for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { ret += app.getDOMText( elem ); } } } else if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } return ret; }; app.getDOMHash = function( elem ) { var nodeType = elem.nodeType, nodeName = elem.nodeName, ret = ''; if ( nodeType === 3 || nodeType === 4 ) { return '#'; } else if ( nodeType === 1 || nodeType === 9 ) { ret += '<' + nodeName + '>'; for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { ret += app.getDOMHash( elem ); } ret += ''; } return ret; }; app.prototype.getOffset = function( localNode, localOffset ) { var $node = $( localNode ); if ( $node.hasClass( 'leafNode' ) ) { return localOffset; } while( !$node.hasClass( 'leafNode' ) ) { $node = $node.parent(); } var current = [$node.contents(), 0]; var stack = [current]; var offset = 0; while ( stack.length > 0 ) { if ( current[1] >= current[0].length ) { stack.pop(); current = stack[ stack.length - 1 ]; continue; } var item = current[0][current[1]]; var $item = current[0].eq( current[1] ); if ( item.nodeType === 3 ) { if ( item === localNode ) { offset += localOffset; break; } else { offset += item.textContent.length; } } else if ( item.nodeType === 1 ) { if ( $( item ).attr('contentEditable') === "false" ) { offset += 1; } else { if ( item === localNode ) { offset += localOffset; break; } stack.push( [$item.contents(), 0] ); current[1]++; current = stack[stack.length-1]; continue; } } current[1]++; } return offset; }; $(function() { new app(); });