mediawiki-extensions-Visual.../modules/ve/ce/ve.ce.SurfaceObserver.js
Trevor Parscal 2e76271b4e The Great ve.ui.Surface refactor of 2013
Prologue:

Farewell ve.Editor my good chap… Oh, hey there HTML frames - I didn't
see you there! In a world where iframes are outlaws, and symbols like
document and window are global, there were more than a few assumptions
about which document or window was being used. But fear not - for this
commit (probably) tracks them all down, leaving a trail of
iframe-compatible awesomeness in its wake. With the great ve.ui.Surface
now able to be used inside of iframes, let the reference editing
commence. But there, lurking in the darkness is a DM issue so fierce it
may take Roan and/or Ed up to 3 whole hours to sort it out.

Note to Roan and/or Ed:

Editing references seems to work fine, but when saving the page there
are "no changes" which is a reasonable indication to the contrary.

Objectives:

* Make it possible to have multiple surfaces be instantiated, get along
  nicely, and be embedded inside of iframes if needed.
* Make reference content editable within a dialog

Approach:

* Move what's left of ve.Editor to ve.ui.Surface and essentially
  obliterate all use of it
* Make even more stuff inherit from ve.Element (long live this.$$)
* Use the correct document or window anywhere it was being assumed to be
  the top level one
* Resolve stacking order issues by removing the excessive use of z-index
  and introducing global and local overlay elements for each editor
* Add a surface to the reference dialog, load up the reference contents
  and save them back on apply
* Actually destroy what we create in ce and ui surfaces
* Add recursive frame offset calculation method to ve.Element
* Moved ve.ce.Surface's getSelectionRect method to the prototype

Bonus:

* Move ve.ce.DocumentNode.css contents to ve.ce.Node.css (not sure why it
  was separate in the first place, but I'm likely the one to blame)
* Fix blatant lies in documentation
* Whitespace cleanup here and there
* Get rid of ve.ui.Window overlays - not used or needed

Change-Id: Iede83e7d24f7cb249b6ba3dc45d770445b862e08
2013-05-24 14:01:02 +02:00

210 lines
5 KiB
JavaScript

/*global rangy */
/*!
* VisualEditor ContentEditable Surface class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* ContentEditable surface observer.
*
* @class
* @mixins ve.EventEmitter
*
* @constructor
* @param {ve.ce.Document} documentView Document to observe
*/
ve.ce.SurfaceObserver = function VeCeSurfaceObserver( documentView ) {
// Mixin constructors
ve.EventEmitter.call( this );
// Properties
this.documentView = documentView;
this.domDocument = null;
this.polling = false;
this.timeoutId = null;
this.frequency = 250; //ms
// Initialization
this.clear();
};
/* Inheritance */
ve.mixinClass( ve.ce.SurfaceObserver, ve.EventEmitter );
/* Events */
/**
* When #poll sees a change this event is emitted (before the
* properties are updated).
*
* @event contentChange
* @param {HTMLElement} node DOM node the change occured in
* @param {Object} previous Old data
* @param {Object} previous.text Old plain text content
* @param {Object} previous.hash Old DOM hash
* @param {ve.Range} previous.range Old selection
* @param {Object} next New data
* @param {Object} next.text New plain text content
* @param {Object} next.hash New DOM hash
* @param {ve.Range} next.range New selection
*/
/**
* When #poll observes a change in the document and the new
* selection does not equal as the last known selection, this event
* is emitted (before the properties are updated).
*
* @event selectionChange
* @param {ve.Range} oldRange
* @param {ve.Range} newRange
*/
/* Methods */
/**
* Clear polling data.
*
* @method
* @param {ve.Range} range Initial range to use
*/
ve.ce.SurfaceObserver.prototype.clear = function ( range ) {
this.rangyRange = null;
this.range = range || null;
this.node = null;
this.text = null;
this.hash = null;
};
/**
* Start 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.domDocument = this.documentView.getDocumentNode().getElementDocument();
this.polling = true;
this.poll( async );
};
/**
* Stop 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
*
* FIXME: Does not work well (selectionChange is not emited) when cursor is placed inside a slug
* with a mouse.
*
* @method
* @param {boolean} async Poll asynchronously
* @emits contentChange
* @emits selectionChange
*/
ve.ce.SurfaceObserver.prototype.poll = function ( async ) {
var delayPoll, $nodeOrSlug, node, text, hash, range, rangyRange;
if ( this.polling === false ) {
return;
}
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;
}
range = this.range;
node = this.node;
rangyRange = ve.ce.DomRange.newFromDomSelection( rangy.getSelection( this.domDocument ) );
if ( !rangyRange.equals( this.rangyRange ) ){
this.rangyRange = rangyRange;
node = null;
$nodeOrSlug = $( rangyRange.anchorNode ).closest( '.ve-ce-branchNode, .ve-ce-branchNode-slug' );
if ( $nodeOrSlug.length ) {
range = rangyRange.getRange();
if ( !$nodeOrSlug.hasClass( 've-ce-branchNode-slug' ) ) {
node = $nodeOrSlug.data( 'view' );
}
}
}
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();
};