From 21056ff5ec0d984e17b2680c5a210a5fc107d7bc Mon Sep 17 00:00:00 2001 From: Inez Korczynski Date: Fri, 8 Jun 2012 15:31:29 -0700 Subject: [PATCH] Support for basic text insertion - typing --- modules/ve2/ce/nodes/ve.ce.DocumentNode.js | 5 +- modules/ve2/ce/nodes/ve.ce.TextNode.js | 24 ++- modules/ve2/ce/ve.ce.Document.js | 4 +- modules/ve2/ce/ve.ce.Surface.js | 224 ++++++++++++++++++++- 4 files changed, 240 insertions(+), 17 deletions(-) diff --git a/modules/ve2/ce/nodes/ve.ce.DocumentNode.js b/modules/ve2/ce/nodes/ve.ce.DocumentNode.js index df3fb9305b..97b3ce4a36 100644 --- a/modules/ve2/ce/nodes/ve.ce.DocumentNode.js +++ b/modules/ve2/ce/nodes/ve.ce.DocumentNode.js @@ -6,10 +6,13 @@ * @extends {ve.ce.BranchNode} * @param model {ve.dm.DocumentNode} Model to observe */ -ve.ce.DocumentNode = function( model ) { +ve.ce.DocumentNode = function( model, surface ) { // Inheritance ve.ce.BranchNode.call( this, 'document', model ); + // Properties + this.surface = surface; + // DOM Changes this.$.addClass( 've-ce-documentNode' ); this.$.attr('contentEditable', 'true'); diff --git a/modules/ve2/ce/nodes/ve.ce.TextNode.js b/modules/ve2/ce/nodes/ve.ce.TextNode.js index f8b38e76e3..c7692577f2 100644 --- a/modules/ve2/ce/nodes/ve.ce.TextNode.js +++ b/modules/ve2/ce/nodes/ve.ce.TextNode.js @@ -14,7 +14,7 @@ ve.ce.TextNode = function( model ) { this.model.addListenerMethod( this, 'update', 'onUpdate' ); // Intialization - this.onUpdate(); + this.onUpdate( true ); }; /* Static Members */ @@ -122,13 +122,15 @@ ve.ce.TextNode.annotationRenderers = { * * @method */ -ve.ce.TextNode.prototype.onUpdate = function() { - var $new = $( $( '' + this.getHtml() + '' ).contents() ); - if ( $new.length === 0 ) { - $new = $new.add( document.createTextNode( '' ) ); +ve.ce.TextNode.prototype.onUpdate = function( force ) { + if ( force === true || this.getSurface().autoRender === true ) { + var $new = $( $( '' + this.getHtml() + '' ).contents() ); + if ( $new.length === 0 ) { + $new = $new.add( document.createTextNode( '' ) ); + } + this.$.replaceWith( $new ); + this.$ = $new; } - this.$.replaceWith( $new ); - this.$ = $new; }; /** @@ -271,6 +273,14 @@ ve.ce.TextNode.prototype.getHtml = function() { return out; }; +ve.ce.TextNode.prototype.getSurface = function() { + var view = this; + while( !view.surface ) { + view = view.parent; + } + return view.surface; +}; + /* Registration */ ve.ce.nodeFactory.register( 'text', ve.ce.TextNode ); diff --git a/modules/ve2/ce/ve.ce.Document.js b/modules/ve2/ce/ve.ce.Document.js index 9bdfcfe744..b34442013a 100644 --- a/modules/ve2/ce/ve.ce.Document.js +++ b/modules/ve2/ce/ve.ce.Document.js @@ -6,9 +6,9 @@ * @constructor * @param model {ve.dm.Document} Model to observe */ -ve.ce.Document = function( model ) { +ve.ce.Document = function( model, surface ) { // Inheritance - ve.Document.call( this, new ve.ce.DocumentNode( model.getDocumentNode() ) ); + ve.Document.call( this, new ve.ce.DocumentNode( model.getDocumentNode(), surface ) ); // Properties this.model = model; diff --git a/modules/ve2/ce/ve.ce.Surface.js b/modules/ve2/ce/ve.ce.Surface.js index b034922d6a..908a78b03e 100644 --- a/modules/ve2/ce/ve.ce.Surface.js +++ b/modules/ve2/ce/ve.ce.Surface.js @@ -11,10 +11,36 @@ ve.ce.Surface = function( $container, model ) { // Properties this.model = model; - this.documentView = new ve.ce.Document( model.getDocument() ); + this.documentView = new ve.ce.Document( model.getDocument(), this ); this.contextView = new ve.ui.Context( this ); this.$ = $container; this.clipboard = {}; + this.autoRender = false; + + this.poll = { + text: null, + hash: null, + node: null, + range: null, + rangySelection: { + anchorNode: null, + anchorOffset: null, + focusNode: null, + focusOffset: null + }, + + polling: false, + timeout: null, + frequency: 100 + }; + + + // Events + this.documentView.documentNode.$.on( { + focus: this.proxy( this.documentOnFocus ), + blur: this.proxy( this.documentOnBlur ) + } ); + this.on( 'contentChange', this.proxy( this.onContentChange ) ); // Driven by mousedown and mouseup events this.isMouseDown = false; @@ -27,14 +53,14 @@ ve.ce.Surface = function( $container, model ) { // init rangy in case of Toshiba... rangy.init(); - + // Events this.$.on( { - 'keypress': this.proxy( this.onKeyPress ), - 'keydown': this.proxy( this.onKeyDown ), - 'mousedown': this.proxy( this.onMouseDown ), - 'mouseup': this.proxy( this.onMouseUp ), - 'mousemove': this.proxy( this.onMouseMove ), + //'keypress': this.proxy( this.onKeyPress ), + //'keydown': this.proxy( this.onKeyDown ), + //'mousedown': this.proxy( this.onMouseDown ), + //'mouseup': this.proxy( this.onMouseUp ), + //'mousemove': this.proxy( this.onMouseMove ), 'cut copy': this.proxy( this.onCutCopy ), 'beforepaste paste': this.proxy( this.onPaste ), 'dragover drop': function( e ) { @@ -43,6 +69,7 @@ ve.ce.Surface = function( $container, model ) { return false; } } ); + this.model.on( 'select', this.proxy( this.onSelect ) ); // Initialization @@ -64,6 +91,142 @@ ve.ce.Surface.prototype.proxy = function( func ) { }); }; +ve.ce.Surface.prototype.documentOnFocus = function() { + console.log( 'documentOnFocus' ); + this.startPolling( true ); +}; + +ve.ce.Surface.prototype.documentOnBlur = function() { + console.log( 'documentOnBlur' ); + this.stopPolling(); +}; + +ve.ce.Surface.prototype.startPolling = function( async ) { + console.log( 'startPolling', async ); + + if ( this.poll.polling === false ) { + this.poll.polling = true; + this.pollChanges( async ); + } +}; + +ve.ce.Surface.prototype.stopPolling = function() { + console.log( 'stopPolling' ); + + if ( this.poll.polling === true ) { + this.poll.polling = false; + clearTimeout( this.poll.timeout ); + } +}; + +ve.ce.Surface.prototype.pollChanges = function( async ) { + var delay = this.proxy( function( now ) { + this.poll.timeout = setTimeout( this.proxy( this.pollChanges ), async ? 0 : this.poll.frequency ); + } ); + + if ( async ) { + delay( true ); + return; + } + + var node = this.poll.node, + range = this.poll.range, + rangySelection = rangy.getSelection(); + + if ( + rangySelection.anchorNode !== this.poll.rangySelection.anchorNode || + rangySelection.anchorOffset !== this.poll.rangySelection.anchorOffset || + rangySelection.focusNode !== this.poll.rangySelection.focusNode || + rangySelection.focusOffset !== this.poll.rangySelection.focusOffset + ) { + this.poll.rangySelection.anchorNode = rangySelection.anchorNode; + this.poll.rangySelection.anchorOffset = rangySelection.anchorOffset; + this.poll.rangySelection.focusNode = rangySelection.focusNode; + this.poll.rangySelection.focusOffset = rangySelection.focusOffset; + + // TODO: Optimize for the case of collapsed (rangySelection.isCollapsed) range + + var $anchorNode = $( rangySelection.anchorNode ).closest( '.ve-ce-branchNode' ), + $focusNode = $( rangySelection.focusNode ).closest( '.ve-ce-branchNode' ); + + if ( $anchorNode[0] === $focusNode[0] ) { + node = $anchorNode[0] + } else { + node = null; + } + + // Do we really need to figure out range even if node is null? + + range = new ve.Range( + this.getOffset( rangySelection.anchorNode, rangySelection.anchorOffset ), + this.getOffset( rangySelection.focusNode, rangySelection.focusOffset ) + ); + } + + if ( this.poll.node !== node ) { + this.poll.text = ve.ce.Surface.getDOMText( node ); + this.poll.hash = ve.ce.Surface.getDOMHash( node ); + this.poll.node = node; + } else { + var text = ve.ce.Surface.getDOMText( node ), + hash = ve.ce.Surface.getDOMHash( node ); + if ( this.poll.text !== text || this.poll.hash !== hash ) { + + this.emit( 'contentChange', { + 'node': node, + 'old': { + 'text': this.poll.text, + 'hash': this.poll.hash, + 'range': this.poll.range, + }, + 'new': { + 'text': text, + 'hash': hash, + 'range': range + } + } ); + + this.poll.text = text; + this.poll.hash = hash; + } + } + + if ( this.poll.range !== range ) { + this.poll.range = range; + this.model.setSelection( range ); + } + + delay(); +}; + +ve.ce.Surface.prototype.onContentChange = function( e ) { + var nodeOffset = $( e.node ).data( 'node' ).model.getOffset(), + offsetDiff = ( + e.old.range !== null && + e.new.range !== null && + e.old.range.getLength() === 0 && + e.new.range.getLength() === 0 + ) ? e.new.range.start - e.old.range.start : null, + lengthDiff = e.new.text.length - e.old.text.length; + + if ( + offsetDiff === lengthDiff && + e.old.text.substring( 0, e.old.range.start - nodeOffset - 1 ) === e.new.text.substring( 0, e.old.range.start - nodeOffset - 1 ) + ) { + var data = e.new.text.substring( e.old.range.start - nodeOffset - 1, e.new.range.start - nodeOffset - 1).split( '' ), + annotations = this.model.getDocument().getAnnotationsFromOffset( e.old.range.start - 1 ); + + // TODO: Add annotations to data + + this.model.transact( ve.dm.Transaction.newFromInsertion( + this.documentView.model, e.old.range.start, data + ) ); + + } else { + // ... + } +}; + ve.ce.Surface.prototype.onSelect = function( e ) { console.log("onSelect", e); }; @@ -179,6 +342,8 @@ ve.ce.Surface.prototype.onCutCopy = function( e ) { $frag = null, key = ''; + this.stopPolling(); + // Create key from text and element names $frag = $(sel.getRangeAt(0).cloneContents()); $frag.contents().each(function() { @@ -202,8 +367,11 @@ ve.ce.Surface.prototype.onCutCopy = function( e ) { selection = _this.model.getSelection(); // Transact + _this.autoRender = true; tx = ve.dm.Transaction.newFromRemoval( _this.documentView.model, selection ); _this.model.transact( tx ); + _this.autoRender = false; + _this.startPolling(); // Place cursor _this.showCursor( selection.start ); @@ -500,6 +668,48 @@ ve.ce.Surface.prototype.getSelectionRect = function() { }; }; +ve.ce.Surface.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 + '>'; + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += ve.ce.Surface.getDOMHash( elem ); + } + ret += ''; + } + return ret; +}; + +ve.ce.Surface.getDOMText = function( elem ) { + var func = 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 += func( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + }; + return func( elem ).replace( /\u00A0|\u0020/g, ' ' ); +}; + /* Inheritance */ ve.extendClass( ve.ce.Surface, ve.EventEmitter );