mediawiki-extensions-Visual.../modules/ve/ce/ve.ce.SurfaceObserver.js
Trevor Parscal de6f4a4254 (bug 41504) ve.ce throws error when surface loses focus
ve.ce.Surface
* Used getModel() as per TODO
* Simplified code by pre-calculating 2 statements that were being used 4 times each
* Re-structured logic so that simple insertion and deletion are only possible if next and previous have a range, triggering complex change otherwise
* Added some documentation

ve.ce.SurfaceObserver
* Added checks for whether rangy found a branch node within the document or not before accessing it's node via .data() and made it so we are only performing DOM > document range conversion if we did
* Removed some commented out code

Change-Id: Iaaf30a5b201710ab4235e775f18808a8d512f1f9
2012-12-03 16:34:27 -08:00

188 lines
4.5 KiB
JavaScript

/*global rangy */
/**
* VisualEditor content editable Surface class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Surface observer.
*
* @class
* @constructor
* @extends {ve.EventEmitter}
* @param {ve.ce.Document} documentView Document to observe
*/
ve.ce.SurfaceObserver = function VeCeSurfaceObserver( documentView ) {
// Parent constructor
ve.EventEmitter.call( this );
// Properties
this.documentView = documentView;
this.polling = false;
this.timeoutId = null;
this.frequency = 250; //ms
// Initialization
this.clear();
};
/* Inheritance */
ve.inheritClass( ve.ce.SurfaceObserver, ve.EventEmitter );
/* Methods */
/**
* Clears polling data.
*
* @method
* @param {ve.Range} range Initial range to use
*/
ve.ce.SurfaceObserver.prototype.clear = function ( range ) {
this.rangySelection = {
anchorNode: null,
anchorOffset: null,
focusNode: null,
focusOffset: null
};
this.range = range || null;
this.node = null;
this.text = null;
this.hash = null;
};
/**
* Starts polling.
*
* If {async} is false or undefined the first poll cycle will occur immediately and synchronously.
*
* @method
* @param {Boolean} async Poll the first time asynchronously
*/
ve.ce.SurfaceObserver.prototype.start = function ( async ) {
this.polling = true;
this.poll( async );
};
/**
* Stops polling.
*
* If {poll} is false or undefined than no final poll cycle will occur and changes can be lost. If
* it's true then a final poll cycle will occur immediately and synchronously.
*
* @method
* @param {Boolean} poll Poll one last time before stopping future polling
*/
ve.ce.SurfaceObserver.prototype.stop = function ( poll ) {
if ( this.polling === true ) {
if ( poll === true ) {
this.poll();
}
this.polling = false;
clearTimeout( this.timeoutId );
this.timeoutId = null;
}
};
/**
* Poll for changes.
*
* If {async} is false or undefined then polling will occcur asynchronously.
*
* TODO: fixing selection in certain cases, handling selection across multiple nodes in Firefox
*
* @method
* @param {Boolean} async Poll asynchronously
*/
ve.ce.SurfaceObserver.prototype.poll = function ( async ) {
var delayPoll, rangySelection, $branch, node, text, hash,
range = null;
if ( this.timeoutId !== null ) {
clearTimeout( this.timeoutId );
this.timeoutId = null;
}
delayPoll = ve.bind( function ( async ) {
this.timeoutId = setTimeout(
ve.bind( this.poll, this ),
async === true ? 0 : this.frequency
);
}, this );
if ( async === true ) {
delayPoll( true );
return;
}
rangySelection = rangy.getSelection();
node = this.node;
if (
rangySelection.anchorNode !== this.rangySelection.anchorNode ||
rangySelection.anchorOffset !== this.rangySelection.anchorOffset ||
rangySelection.focusNode !== this.rangySelection.focusNode ||
rangySelection.focusOffset !== this.rangySelection.focusOffset
) {
this.rangySelection.anchorNode = rangySelection.anchorNode;
this.rangySelection.anchorOffset = rangySelection.anchorOffset;
this.rangySelection.focusNode = rangySelection.focusNode;
this.rangySelection.focusOffset = rangySelection.focusOffset;
$branch = $( rangySelection.anchorNode ).closest( '.ve-ce-branchNode' );
if ( $branch.length ) {
node = $branch.data( 'node' );
if ( node.canHaveGrandchildren() ) {
node = null;
} else {
range = new ve.Range(
ve.ce.getOffset( rangySelection.anchorNode, rangySelection.anchorOffset ),
ve.ce.getOffset( rangySelection.focusNode, rangySelection.focusOffset )
);
}
}
}
if ( this.node !== node ) {
if ( node === null ) {
this.text = null;
this.hash = null;
this.node = null;
} else {
this.text = ve.ce.getDomText( node.$[0] );
this.hash = ve.ce.getDomHash( node.$[0] );
this.node = node;
}
} else {
if ( node !== null ) {
text = ve.ce.getDomText( node.$[0] );
hash = ve.ce.getDomHash( node.$[0] );
if ( this.text !== text || this.hash !== hash ) {
this.emit(
'contentChange',
node,
{ 'text': this.text, 'hash': this.hash, 'range': this.range },
{ 'text': text, 'hash': hash, 'range': range }
);
this.text = text;
this.hash = hash;
}
}
}
// Only emit selectionChange event if there's a meaningful range difference
if ( ( this.range && range ) ? !this.range.equals( range ) : ( this.range !== range ) ) {
this.emit(
'selectionChange',
this.range,
range
);
this.range = range;
}
delayPoll();
};