mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-12 09:09:25 +00:00
Start using ve.ce.SurfaceObserver inside ve.ce.Surface. Moved methods getOffsetOfSlug, getOffsetFromElementNode, getOffsetFromTextNode and getOffset to ve.ce.js and made them static.
Change-Id: If6ce0ae10c494392cc384341cc87b61658d4934a
This commit is contained in:
parent
c4299fd838
commit
72eb2825e5
|
@ -24,6 +24,7 @@ ve.ce.Surface = function VeCeSurface( $container, model ) {
|
|||
this.model = model;
|
||||
this.documentView = null; // See initialization below
|
||||
this.contextView = null; // See initialization below
|
||||
this.surfaceObserver = null; // See initialization below
|
||||
this.selectionTimeout = null;
|
||||
this.$ = $container;
|
||||
this.$document = $( document );
|
||||
|
@ -31,22 +32,6 @@ ve.ce.Surface = function VeCeSurface( $container, model ) {
|
|||
this.locked = false;
|
||||
this.sluggable = true;
|
||||
|
||||
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.model.addListenerMethods(
|
||||
this, { 'change': 'onChange', 'lock': 'onLock', 'unlock': 'onUnlock' }
|
||||
|
@ -79,6 +64,7 @@ ve.ce.Surface = function VeCeSurface( $container, model ) {
|
|||
// Must be initialized after select and transact listeners are added to model so respond first
|
||||
this.documentView = new ve.ce.Document( model.getDocument(), this );
|
||||
this.contextView = new ve.ui.Context( this );
|
||||
this.surfaceObserver = new ve.ce.SurfaceObserver( this.documentView );
|
||||
this.$.append( this.documentView.getDocumentNode().$ );
|
||||
|
||||
// DocumentNode Events
|
||||
|
@ -86,6 +72,10 @@ ve.ce.Surface = function VeCeSurface( $container, model ) {
|
|||
'focus': ve.bind( this.documentOnFocus, this ),
|
||||
'blur': ve.bind( this.documentOnBlur, this )
|
||||
} );
|
||||
|
||||
// SurfaceObserver Events
|
||||
this.surfaceObserver.on( 'contentChange', ve.bind( this.onContentChange, this ) );
|
||||
this.surfaceObserver.on( 'selectionChange', ve.bind( this.onSelectionChange, this ) );
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
@ -94,6 +84,130 @@ ve.inheritClass( ve.ce.Surface, ve.EventEmitter );
|
|||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Responds to 'contentChange' events emitted in {ve.ce.SurfaceObserver.prototype.poll}.
|
||||
*
|
||||
* @method
|
||||
* @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 {Object} 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 {Object} next.range New selection
|
||||
*/
|
||||
ve.ce.Surface.prototype.onContentChange = function ( node, previous, next ) {
|
||||
var nodeOffset = node.model.getOffset(), // TODO: call getModel() or add getOffset() to view
|
||||
offsetDiff = ( previous.range.isCollapsed() && next.range.isCollapsed() )
|
||||
? next.range.start - previous.range.start : null,
|
||||
lengthDiff = next.text.length - previous.text.length,
|
||||
sameLeadingAndTrailing = offsetDiff !== null && ( // TODO: rewrite to static method with tests
|
||||
(
|
||||
lengthDiff > 0 &&
|
||||
previous.text.substring( 0, previous.range.start - nodeOffset - 1 ) ===
|
||||
next.text.substring( 0, previous.range.start - nodeOffset - 1 ) &&
|
||||
previous.text.substring( previous.range.start - nodeOffset - 1 ) ===
|
||||
next.text.substring( next.range.start - nodeOffset - 1 )
|
||||
) ||
|
||||
(
|
||||
lengthDiff < 0 &&
|
||||
previous.text.substring( 0, next.range.start - nodeOffset - 1 ) ===
|
||||
next.text.substring( 0, next.range.start - nodeOffset - 1 ) &&
|
||||
previous.text.substring( previous.range.start - nodeOffset - 1 - lengthDiff + offsetDiff) ===
|
||||
next.text.substring( next.range.start - nodeOffset - 1 )
|
||||
)
|
||||
),
|
||||
data,
|
||||
range,
|
||||
len,
|
||||
fromLeft = 0,
|
||||
fromRight = 0,
|
||||
transactions = [],
|
||||
complex;
|
||||
|
||||
if ( lengthDiff > 0 && offsetDiff === lengthDiff /* && sameLeadingAndTrailing */) {
|
||||
data = next.text.substring(
|
||||
previous.range.start - nodeOffset - 1,
|
||||
next.range.start - nodeOffset - 1
|
||||
).split( '' );
|
||||
// Apply insertAnnotations
|
||||
ve.dm.Document.addAnnotationsToData( data, this.model.getDocument().insertAnnotations );
|
||||
this.lock();
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromInsertion(
|
||||
this.documentView.model, previous.range.start, data
|
||||
),
|
||||
next.range
|
||||
);
|
||||
this.unlock();
|
||||
} else if ( ( offsetDiff === 0 || offsetDiff === lengthDiff ) && sameLeadingAndTrailing ) {
|
||||
if ( offsetDiff === 0 ) {
|
||||
range = new ve.Range( next.range.start, next.range.start - lengthDiff );
|
||||
} else {
|
||||
range = new ve.Range( next.range.start, previous.range.start );
|
||||
}
|
||||
this.lock();
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromRemoval( this.documentView.model, range ),
|
||||
next.range
|
||||
);
|
||||
this.unlock();
|
||||
} else {
|
||||
len = Math.min( previous.text.length, next.text.length );
|
||||
// Count same characters from left
|
||||
while ( fromLeft < len && previous.text[fromLeft] === next.text[fromLeft] ) {
|
||||
++fromLeft;
|
||||
}
|
||||
// Count same characters from right
|
||||
while (
|
||||
fromRight < len - fromLeft &&
|
||||
previous.text[previous.text.length - 1 - fromRight] ===
|
||||
next.text[next.text.length - 1 - fromRight]
|
||||
) {
|
||||
++fromRight;
|
||||
}
|
||||
data = next.text.substring( fromLeft, next.text.length - fromRight ).split( '' );
|
||||
|
||||
// Get annotations to the left of new content and apply
|
||||
annotations = this.model.getDocument().getAnnotationsFromOffset( nodeOffset + 1 + fromLeft );
|
||||
if ( annotations.getLength() > 0 ) {
|
||||
ve.dm.Document.addAnnotationsToData( data, annotations );
|
||||
}
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromInsertion(
|
||||
this.documentView.model, nodeOffset + 1 + fromLeft, data
|
||||
),
|
||||
next.range
|
||||
);
|
||||
if ( fromLeft + fromRight < previous.text.length ) {
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromRemoval(
|
||||
this.documentView.model,
|
||||
new ve.Range(
|
||||
data.length + nodeOffset + 1 + fromLeft,
|
||||
data.length + nodeOffset + 1 + previous.text.length - fromRight
|
||||
)
|
||||
),
|
||||
next.range
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to 'selectionChange' events emitted in {ve.ce.SurfaceObserver.prototype.poll}.
|
||||
*
|
||||
* @method
|
||||
*/
|
||||
ve.ce.Surface.prototype.onSelectionChange = function ( oldRange, newRange ) {
|
||||
// TODO: Explain why we lock here.
|
||||
this.lock();
|
||||
this.model.change( null, newRange );
|
||||
this.unlock();
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to surface lock events.
|
||||
*
|
||||
|
@ -120,18 +234,16 @@ ve.ce.Surface.prototype.onUnlock = function () {
|
|||
* @param {jQuery.Event} e
|
||||
*/
|
||||
ve.ce.Surface.prototype.documentOnFocus = function () {
|
||||
ve.log( 'documentOnFocus' );
|
||||
|
||||
this.$document.off( '.ve-ce-Surface' );
|
||||
|
||||
this.$document.on( {
|
||||
// key down
|
||||
'keydown.ve-ce-Surface': ve.bind( this.onKeyDown, this ),
|
||||
'keypress.ve-ce-Surface': ve.bind( this.onKeyPress, this ),
|
||||
// mouse down
|
||||
'mousedown.ve-ce-Surface': ve.bind( this.onMouseDown, this )
|
||||
'mousedown.ve-ce-Surface': ve.bind( this.onMouseDown, this ),
|
||||
'mouseup.ve-ce-Surface': ve.bind( this.onMouseUp, this ),
|
||||
//'compositionstart.ve-ce-Surface': ve.bind( this.onCompositionStart, this ),
|
||||
//'compositionend.ve-ce-Surface': ve.bind( this.onCompositionEnd, this ),
|
||||
} );
|
||||
this.startPolling( true );
|
||||
this.surfaceObserver.start( true );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -141,14 +253,10 @@ ve.ce.Surface.prototype.documentOnFocus = function () {
|
|||
* @param {jQuery.Event} e
|
||||
*/
|
||||
ve.ce.Surface.prototype.documentOnBlur = function () {
|
||||
ve.log( 'documentOnBlur' );
|
||||
|
||||
this.$document.off( '.ve-ce-Surface' );
|
||||
this.stopPolling();
|
||||
if (
|
||||
this.contextView &&
|
||||
!this.contextView.areChildrenCurrentlyVisible()
|
||||
) {
|
||||
this.surfaceObserver.stop( true );
|
||||
|
||||
if ( this.contextView && !this.contextView.areChildrenCurrentlyVisible() ) {
|
||||
this.contextView.clear();
|
||||
}
|
||||
};
|
||||
|
@ -160,15 +268,11 @@ ve.ce.Surface.prototype.documentOnBlur = function () {
|
|||
* @param {jQuery.Event} e
|
||||
*/
|
||||
ve.ce.Surface.prototype.onMouseDown = function ( e ) {
|
||||
ve.log( 'onMouseDown' );
|
||||
// Old code to figure out if user clicked inside the document or not - leave it here for now
|
||||
// $( e.target ).closest( '.ve-ce-documentNode' ).length === 0
|
||||
|
||||
if ( this.poll.polling === true ) {
|
||||
this.pollChanges();
|
||||
if ( $( e.target ).closest( '.ve-ce-documentNode' ).length === 0 ) {
|
||||
this.stopPolling();
|
||||
} else {
|
||||
this.pollChanges( true );
|
||||
}
|
||||
if ( e.which === 1 ) {
|
||||
this.surfaceObserver.stop( true );
|
||||
}
|
||||
|
||||
// Block / prevent triple click
|
||||
|
@ -177,6 +281,16 @@ ve.ce.Surface.prototype.onMouseDown = function ( e ) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to document mouse up events.
|
||||
*
|
||||
* @method
|
||||
* @param {jQuery.Event} e
|
||||
*/
|
||||
ve.ce.Surface.prototype.onMouseUp = function ( e ) {
|
||||
this.surfaceObserver.start();
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to document key down events.
|
||||
*
|
||||
|
@ -333,10 +447,11 @@ ve.ce.Surface.prototype.onKeyDown = function ( e ) {
|
|||
}
|
||||
break;
|
||||
default:
|
||||
if ( this.poll.polling === false ) {
|
||||
this.poll.polling = true;
|
||||
this.pollChanges();
|
||||
}
|
||||
// TODO: Filter (do not call stop and start) for [a-zA-Z0-9]
|
||||
//if ( this.model.getSelection().isCollapsed() === false ) {
|
||||
this.surfaceObserver.stop(true);
|
||||
this.surfaceObserver.start();
|
||||
//}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -512,6 +627,7 @@ ve.ce.Surface.prototype.onPaste = function () {
|
|||
* @param {jQuery.Event} e
|
||||
*/
|
||||
ve.ce.Surface.prototype.onKeyPress = function ( e ) {
|
||||
return;
|
||||
var node, selection, data;
|
||||
|
||||
ve.log( 'onKeyPress' );
|
||||
|
@ -568,311 +684,6 @@ ve.ce.Surface.prototype.onKeyPress = function ( e ) {
|
|||
|
||||
};
|
||||
|
||||
/**
|
||||
* Begins polling for changes.
|
||||
*
|
||||
* @method
|
||||
* @param {Boolean} async Allow polling to happen asynchronously
|
||||
*/
|
||||
ve.ce.Surface.prototype.startPolling = function ( async ) {
|
||||
ve.log( 'startPolling' );
|
||||
|
||||
this.poll.polling = true;
|
||||
this.pollChanges( async );
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops polling for changes.
|
||||
*
|
||||
* @method
|
||||
*/
|
||||
ve.ce.Surface.prototype.stopPolling = function () {
|
||||
ve.log( 'stopPolling' );
|
||||
|
||||
this.poll.polling = false;
|
||||
clearTimeout( this.poll.timeout );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears change-polling state.
|
||||
*
|
||||
* @method
|
||||
*/
|
||||
ve.ce.Surface.prototype.clearPollData = function () {
|
||||
ve.log( 'clearPollData' );
|
||||
|
||||
this.poll.text = null;
|
||||
this.poll.hash = null;
|
||||
this.poll.node = null;
|
||||
this.poll.rangySelection.anchorNode = null;
|
||||
this.poll.rangySelection.anchorOffset = null;
|
||||
this.poll.rangySelection.focusNode = null;
|
||||
this.poll.rangySelection.focusOffset = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the document has changed since last poll.
|
||||
*
|
||||
* @method
|
||||
*/
|
||||
ve.ce.Surface.prototype.pollChanges = function ( async ) {
|
||||
var delay, node, range, rangySelection,
|
||||
$anchorNode, $focusNode,
|
||||
text, hash;
|
||||
|
||||
delay = ve.bind( function ( async ) {
|
||||
if ( this.poll.polling ) {
|
||||
if ( this.poll.timeout !== null ) {
|
||||
clearTimeout( this.poll.timeout );
|
||||
}
|
||||
this.poll.timeout = setTimeout(
|
||||
ve.bind( this.pollChanges, this ), async ? 0 : this.poll.frequency
|
||||
);
|
||||
}
|
||||
}, this );
|
||||
|
||||
if ( async ) {
|
||||
delay( true );
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
// TODO: Do we really need to figure out range even if node is null? (YES)
|
||||
|
||||
range = new ve.Range(
|
||||
this.getOffset( rangySelection.anchorNode, rangySelection.anchorOffset ),
|
||||
this.getOffset( rangySelection.focusNode, rangySelection.focusOffset )
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Invastigate more when and why node is null and what to do in those cases
|
||||
|
||||
if ( this.poll.node !== node ) {
|
||||
if ( node === null ) {
|
||||
this.poll.text = this.poll.hash = this.poll.node = null;
|
||||
} else {
|
||||
this.poll.text = ve.ce.getDomText( node );
|
||||
this.poll.hash = ve.ce.getDomHash( node );
|
||||
this.poll.node = node;
|
||||
}
|
||||
} else {
|
||||
if ( node !== null ) {
|
||||
text = ve.ce.getDomText( node );
|
||||
hash = ve.ce.getDomHash( node );
|
||||
if ( this.poll.text !== text || this.poll.hash !== hash ) {
|
||||
this.emit(
|
||||
'contentChange',
|
||||
node,
|
||||
{
|
||||
'text': this.poll.text,
|
||||
'hash': this.poll.hash,
|
||||
'range': this.poll.range
|
||||
},
|
||||
{
|
||||
'text': text,
|
||||
'hash': hash,
|
||||
'range': range
|
||||
}
|
||||
);
|
||||
this.poll.text = text;
|
||||
this.poll.hash = hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( this.poll.range !== range ) {
|
||||
|
||||
// TODO: Fix range
|
||||
if ( range.getLength() === 0 ) {
|
||||
range = new ve.Range( this.getNearestCorrectOffset( range.start, 1 ) );
|
||||
}
|
||||
|
||||
this.model.change( null, range );
|
||||
this.poll.range = range;
|
||||
}
|
||||
|
||||
delay();
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to document content change events.
|
||||
*
|
||||
* Emitted in {ve.ce.Surface.prototype.pollChanges}.
|
||||
*
|
||||
* @method
|
||||
* @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 {Object} 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 {Object} next.range New selection
|
||||
*/
|
||||
ve.ce.Surface.prototype.onContentChange = function ( node, previous, next ) {
|
||||
var data, annotations, len, range,
|
||||
nodeOffset = $( node ).data( 'node' ).model.getOffset(),
|
||||
offsetDiff = (
|
||||
previous.range !== null &&
|
||||
next.range !== null &&
|
||||
previous.range.getLength() === 0 &&
|
||||
next.range.getLength() === 0
|
||||
) ? next.range.start - previous.range.start : null,
|
||||
lengthDiff = next.text.length - previous.text.length,
|
||||
fromLeft = 0,
|
||||
fromRight = 0,
|
||||
somethingIsFishy = !(
|
||||
(
|
||||
// Adding text
|
||||
lengthDiff > 0 &&
|
||||
// Leading and trailing chars are the same
|
||||
previous.text.substring( 0, previous.range.start - nodeOffset - lengthDiff ) ===
|
||||
next.text.substring( 0, previous.range.start - nodeOffset - lengthDiff ) &&
|
||||
previous.text.substring( previous.range.start - nodeOffset + lengthDiff ) ===
|
||||
next.text.substring( next.range.start - nodeOffset + lengthDiff )
|
||||
) ||
|
||||
(
|
||||
// Removing text
|
||||
lengthDiff < 0 &&
|
||||
// Leading and trailing chars are the same
|
||||
previous.text.substring( 0, next.range.start - nodeOffset -1 ) ===
|
||||
next.text.substring( 0, next.range.start - nodeOffset - 1 ) &&
|
||||
previous.text.substring( next.range.start - nodeOffset - lengthDiff - 1 ) ===
|
||||
next.text.substring( next.range.start - nodeOffset - 1 )
|
||||
)
|
||||
);
|
||||
if (
|
||||
lengthDiff > 0 &&
|
||||
offsetDiff === lengthDiff &&
|
||||
!somethingIsFishy
|
||||
) {
|
||||
// Something simple was added, figure out what it is and transact.
|
||||
ve.log('simple addition');
|
||||
|
||||
data = next.text.substring(
|
||||
previous.range.start - nodeOffset - 1,
|
||||
next.range.start - nodeOffset - 1
|
||||
).split( '' );
|
||||
|
||||
// Apply insertAnnotations
|
||||
ve.dm.Document.addAnnotationsToData( data, this.model.getDocument().insertAnnotations );
|
||||
|
||||
// Prevent re-rendering and transact
|
||||
this.lock();
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromInsertion(
|
||||
this.documentView.model, previous.range.start, data
|
||||
),
|
||||
next.range
|
||||
);
|
||||
this.unlock();
|
||||
|
||||
} else if (
|
||||
( offsetDiff === 0 || offsetDiff === lengthDiff ) &&
|
||||
!somethingIsFishy
|
||||
) {
|
||||
// Something simple was removed
|
||||
ve.log('simple deletion');
|
||||
|
||||
// Figure out range
|
||||
range = null;
|
||||
if ( offsetDiff === 0 ) {
|
||||
ve.log('delete');
|
||||
range = new ve.Range( previous.range.start, next.range.start - lengthDiff );
|
||||
} else {
|
||||
ve.log('backspace');
|
||||
range = new ve.Range( previous.range.start, next.range.start );
|
||||
}
|
||||
|
||||
// Prevent re-rendering and transact
|
||||
this.lock();
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromRemoval( this.documentView.model, range ),
|
||||
next.range
|
||||
);
|
||||
this.unlock();
|
||||
|
||||
} else {
|
||||
ve.log('complex text change');
|
||||
len = Math.min( previous.text.length, next.text.length );
|
||||
|
||||
// Count same characters from left
|
||||
while ( fromLeft < len && previous.text[fromLeft] === next.text[fromLeft] ) {
|
||||
++fromLeft;
|
||||
}
|
||||
// Count same characters from right
|
||||
while (
|
||||
fromRight < len - fromLeft &&
|
||||
previous.text[previous.text.length - 1 - fromRight] ===
|
||||
next.text[next.text.length - 1 - fromRight]
|
||||
) {
|
||||
++fromRight;
|
||||
}
|
||||
|
||||
data = next.text.substring( fromLeft, next.text.length - fromRight ).split( '' );
|
||||
|
||||
// Get annotations to the left of new content and apply
|
||||
annotations = this.model.getDocument().getAnnotationsFromOffset( nodeOffset + 1 + fromLeft );
|
||||
if ( annotations.getLength() > 0 ) {
|
||||
ve.dm.Document.addAnnotationsToData( data, annotations );
|
||||
}
|
||||
|
||||
this.clearPollData();
|
||||
|
||||
// TODO: combine newFromRemoval and newFromInsertion into one newFromReplacement
|
||||
if ( fromLeft + fromRight < previous.text.length ) {
|
||||
// Don't set the selection here: next.range might be out of bounds after
|
||||
// the removal
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromRemoval(
|
||||
this.documentView.model,
|
||||
new ve.Range(
|
||||
nodeOffset + 1 + fromLeft, nodeOffset + 1 + previous.text.length - fromRight
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
this.model.change(
|
||||
ve.dm.Transaction.newFromInsertion(
|
||||
this.documentView.model, nodeOffset + 1 + fromLeft, data
|
||||
),
|
||||
next.range
|
||||
);
|
||||
}
|
||||
this.sluggable = true;
|
||||
|
||||
// TODO: Not sure if this is needed
|
||||
setTimeout( ve.bind( this.pollChanges, this ), 1);
|
||||
|
||||
// ve.log('onContentChange');
|
||||
};
|
||||
|
||||
/**
|
||||
* Called from ve.dm.Surface.prototype.change.
|
||||
*
|
||||
|
@ -881,9 +692,7 @@ ve.ce.Surface.prototype.onContentChange = function ( node, previous, next ) {
|
|||
* @param {ve.Range|undefined} selection
|
||||
*/
|
||||
ve.ce.Surface.prototype.onChange = function ( transaction, selection ) {
|
||||
// ve.log( 'onChange' );
|
||||
|
||||
if ( selection ) {
|
||||
if ( selection && !this.isLocked() ) {
|
||||
this.showSelection( selection );
|
||||
|
||||
// Responsible for Debouncing the ContextView Icon until select events are finished being
|
||||
|
@ -1362,130 +1171,6 @@ ve.ce.Surface.prototype.getNodeAndOffset = function ( offset ) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the linear offset from a given DOM node and offset within it.
|
||||
*
|
||||
* @method
|
||||
* @param {DOMElement} domNode DOM node
|
||||
* @param {Number} domOffset DOM offset within the DOM Element
|
||||
* @returns {Number} Linear model offset
|
||||
*/
|
||||
ve.ce.Surface.prototype.getOffset = function ( domNode, domOffset ) {
|
||||
if ( domNode.nodeType === Node.TEXT_NODE ) {
|
||||
return this.getOffsetFromTextNode( domNode, domOffset );
|
||||
} else {
|
||||
return this.getOffsetFromElementNode( domNode, domOffset );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the linear offset from a given text node and offset within it.
|
||||
*
|
||||
* @method
|
||||
* @param {DOMElement} domNode DOM node
|
||||
* @param {Number} domOffset DOM offset within the DOM Element
|
||||
* @returns {Number} Linear model offset
|
||||
*/
|
||||
ve.ce.Surface.prototype.getOffsetFromTextNode = function ( domNode, domOffset ) {
|
||||
var $node, nodeModel, current, stack, item, offset, $item;
|
||||
|
||||
$node = $( domNode ).closest(
|
||||
'.ve-ce-branchNode, .ve-ce-alienBlockNode, .ve-ce-alienInlineNode'
|
||||
);
|
||||
nodeModel = $node.data( 'node' ).getModel();
|
||||
|
||||
if ( ! $node.hasClass( 've-ce-branchNode' ) ) {
|
||||
return nodeModel.getOffset();
|
||||
}
|
||||
|
||||
current = [$node.contents(), 0];
|
||||
stack = [current];
|
||||
offset = 0;
|
||||
|
||||
while ( stack.length > 0 ) {
|
||||
if ( current[1] >= current[0].length ) {
|
||||
stack.pop();
|
||||
current = stack[ stack.length - 1 ];
|
||||
continue;
|
||||
}
|
||||
item = current[0][current[1]];
|
||||
if ( item.nodeType === Node.TEXT_NODE ) {
|
||||
if ( item === domNode ) {
|
||||
offset += domOffset;
|
||||
break;
|
||||
} else {
|
||||
offset += item.textContent.length;
|
||||
}
|
||||
} else if ( item.nodeType === Node.ELEMENT_NODE ) {
|
||||
$item = current[0].eq( current[1] );
|
||||
if ( $item.hasClass( 've-ce-slug' ) ) {
|
||||
if ( $item.contents()[0] === domNode ) {
|
||||
break;
|
||||
}
|
||||
} else if ( $item.hasClass( 've-ce-leafNode' ) ) {
|
||||
offset += 2;
|
||||
} else if ( $item.hasClass( 've-ce-branchNode' ) ) {
|
||||
offset += $item.data( 'node' ).getOuterLength();
|
||||
} else {
|
||||
stack.push( [$item.contents(), 0 ] );
|
||||
current[1]++;
|
||||
current = stack[stack.length-1];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
current[1]++;
|
||||
}
|
||||
return offset + nodeModel.getOffset() + ( nodeModel.isWrapped() ? 1 : 0 );
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the linear offset from a given element node and offset within it.
|
||||
*
|
||||
* @method
|
||||
* @param {DOMElement} domNode DOM node
|
||||
* @param {Number} domOffset DOM offset within the DOM Element
|
||||
* @param {Boolean} [addOuterLength] Use outer length, which includes wrappers if any exist
|
||||
* @returns {Number} Linear model offset
|
||||
*/
|
||||
ve.ce.Surface.prototype.getOffsetFromElementNode = function ( domNode, domOffset, addOuterLength ) {
|
||||
var $domNode = $( domNode ),
|
||||
nodeModel,
|
||||
node;
|
||||
|
||||
if ( $domNode.hasClass( 've-ce-slug' ) ) {
|
||||
if ( $domNode.prev().length ) {
|
||||
nodeModel = $domNode.prev().data( 'node' ).getModel();
|
||||
return nodeModel.getOffset() + nodeModel.getOuterLength();
|
||||
}
|
||||
if ( $domNode.next().length ) {
|
||||
nodeModel = $domNode.next().data( 'node' ).getModel();
|
||||
return nodeModel.getOffset();
|
||||
}
|
||||
}
|
||||
|
||||
if ( domOffset === 0 ) {
|
||||
node = $domNode.data( 'node' );
|
||||
if ( node ) {
|
||||
nodeModel = $domNode.data( 'node' ).getModel();
|
||||
if ( addOuterLength === true ) {
|
||||
return nodeModel.getOffset() + nodeModel.getOuterLength();
|
||||
} else {
|
||||
return nodeModel.getOffset();
|
||||
}
|
||||
} else {
|
||||
node = $domNode.contents().last()[0];
|
||||
}
|
||||
} else {
|
||||
node = $domNode.contents()[ domOffset - 1 ];
|
||||
}
|
||||
|
||||
if ( node.nodeType === Node.TEXT_NODE ) {
|
||||
return this.getOffsetFromTextNode( node, node.length );
|
||||
} else {
|
||||
return this.getOffsetFromElementNode( node, 0, true );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the context icon.
|
||||
*
|
||||
|
|
|
@ -103,3 +103,129 @@ ve.ce.getDomHash = function ( element ) {
|
|||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the linear offset from a given DOM node and offset within it.
|
||||
*
|
||||
* @static
|
||||
* @member
|
||||
* @param {DOM Node} domNode DOM node
|
||||
* @param {Integer} domOffset DOM offset within the DOM node
|
||||
* @returns {Integer} Linear model offset
|
||||
*/
|
||||
ve.ce.getOffset = function ( domNode, domOffset ) {
|
||||
if ( domNode.nodeType === Node.TEXT_NODE ) {
|
||||
return ve.ce.getOffsetFromTextNode( domNode, domOffset );
|
||||
} else if ( domNode.nodeType === Node.ELEMENT_NODE ) {
|
||||
return ve.ce.getOffsetFromElementNode( domNode, domOffset );
|
||||
} else {
|
||||
throw "Unknown node type.";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the linear offset from a given text node and offset within it.
|
||||
* TODO: Consider using .childNodes instead of .contents() for small performance improvement.
|
||||
*
|
||||
* @static
|
||||
* @member
|
||||
* @param {DOM Node} domNode DOM text node
|
||||
* @param {Integer} domOffset DOM offset within the DOM text node
|
||||
* @returns {Integer} Linear model offset
|
||||
*/
|
||||
ve.ce.getOffsetFromTextNode = function ( domNode, domOffset ) {
|
||||
var $node, model, current, stack, offset, item, $item;
|
||||
|
||||
$node = $( domNode ).closest( '.ve-ce-branchNode, .ve-ce-slug, .ve-ce-alienBlockNode, .ve-ce-alienInlineNode' );
|
||||
if ( $node.hasClass( 've-ce-slug' ) ) {
|
||||
return ve.ce.getOffsetOfSlug( $node );
|
||||
}
|
||||
|
||||
model = $node.data( 'node' ).getModel();
|
||||
if ( ! $node.hasClass( 've-ce-branchNode' ) ) {
|
||||
return model.getOffset();
|
||||
}
|
||||
|
||||
current = [ $node.contents(), 0 ];
|
||||
stack = [ current ];
|
||||
offset = 0;
|
||||
|
||||
while ( stack.length > 0 ) {
|
||||
if ( current[1] >= current[0].length ) {
|
||||
stack.pop();
|
||||
current = stack[ stack.length - 1 ];
|
||||
continue;
|
||||
}
|
||||
item = current[0][current[1]];
|
||||
if ( item.nodeType === Node.TEXT_NODE ) {
|
||||
if ( item === domNode ) {
|
||||
offset += domOffset;
|
||||
break;
|
||||
} else {
|
||||
offset += item.textContent.length;
|
||||
}
|
||||
} else if ( item.nodeType === Node.ELEMENT_NODE ) {
|
||||
$item = current[0].eq( current[1] );
|
||||
if ( $item.hasClass( 've-ce-slug' ) ) {
|
||||
if ( $item.contents()[0] === domNode ) {
|
||||
break;
|
||||
}
|
||||
} else if ( $item.hasClass( 've-ce-leafNode' ) ) {
|
||||
offset += $item.data( 'node' ).getOuterLength();
|
||||
} else {
|
||||
stack.push( [ $item.contents(), 0 ] );
|
||||
current[1]++;
|
||||
current = stack[ stack.length-1 ];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
current[1]++;
|
||||
}
|
||||
return offset + model.getOffset() + ( model.isWrapped() ? 1 : 0 );
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the linear offset from a given element node and offset within it.
|
||||
*
|
||||
* @static
|
||||
* @member
|
||||
* @param {DOM Node} domNode DOM element node
|
||||
* @param {Integer} domOffset DOM offset within the DOM element node
|
||||
* @returns {Integer} Linear model offset
|
||||
*/
|
||||
ve.ce.getOffsetFromElementNode = function ( domNode, domOffset ) {
|
||||
var $node = $( domNode );
|
||||
if ( $node.hasClass( 've-ce-slug' ) ) {
|
||||
return ve.ce.getOffsetOfSlug( $node );
|
||||
} else if ( domOffset === 0 ) {
|
||||
return $node.data( 'node' ).getModel().getOffset();
|
||||
} else {
|
||||
$node = $node.contents().eq( domOffset - 1 );
|
||||
if ( $node[0].nodeType === Node.TEXT_NODE ) {
|
||||
return ve.ce.getOffsetFromTextNode( $node[0], 0 );
|
||||
} else if ( $node[0].nodeType === Node.ELEMENT_NODE ) {
|
||||
return $node.data( 'node' ).getModel().getOffset();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the linear offset of a given slug
|
||||
*
|
||||
* @static
|
||||
* @member
|
||||
* @param {jQuery} $node jQuery slug selection
|
||||
* @returns {Integer} Linear model offset
|
||||
*/
|
||||
ve.ce.getOffsetOfSlug = function ( $node ) {
|
||||
var model;
|
||||
if ( $node.index() === 0 ) {
|
||||
model = $node.parent().data( 'node' ).getModel();
|
||||
return model.getOffset() + ( model.isWrapped() ? 1 : 0 );
|
||||
} else if ( $node.prev().length ) {
|
||||
model = $node.prev().data( 'node' ).getModel();
|
||||
return model.getOffset() + model.getOuterLength();
|
||||
} else {
|
||||
throw "Incorrect slug location";
|
||||
}
|
||||
};
|
Loading…
Reference in a new issue