/*!
* VisualEditor Content Editable Range State class
*
* @copyright See AUTHORS.txt
*/
/**
* ContentEditable range state (a snapshot of CE selection/content state)
*
* @class
*
* @constructor
* @param {ve.ce.RangeState|null} old Previous range state
* @param {ve.ce.BranchNode} root Surface root
* @param {boolean} selectionOnly The caller promises the content has not changed from old
*/
ve.ce.RangeState = function VeCeRangeState( old, root, selectionOnly ) {
/**
* @property {boolean} branchNodeChanged Whether the CE branch node changed
*/
this.branchNodeChanged = false;
/**
* @property {boolean} selectionChanged Whether the DOM range changed
*/
this.selectionChanged = false;
/**
* @property {boolean} contentChanged Whether the content changed
*
* This is only set to true if both the old and new states have the
* same current branch node, whose content has changed
*/
this.contentChanged = false;
/**
* @property {ve.Range|null} veRange The current selection range
*/
this.veRange = null;
/**
* @property {ve.ce.BranchNode|null} node The current branch node
*/
this.node = null;
/**
* @property {string|null} text Plain text of current branch node
*/
this.text = null;
/**
* @property {string|null} DOM Hash of current branch node
*/
this.hash = null;
/**
* @property {ve.ce.TextState|null} Current branch node's annotated content
*/
this.textState = null;
/**
* @property {boolean|null} focusIsAfterAnnotationBoundary Focus lies after annotation tag
*/
this.focusIsAfterAnnotationBoundary = null;
/**
* Saved selection for future comparisons. (But it is not properly frozen, because the
* nodes are live and mutable, and therefore the offsets may come to point to places that
* are misleadingly different from when the selection was saved).
*
* @property {ve.SelectionState} misleadingSelection Saved selection (but with live nodes)
*/
this.misleadingSelection = null;
this.saveState( old, root, selectionOnly );
};
/* Inheritance */
OO.initClass( ve.ce.RangeState );
/* Methods */
/**
* Saves a snapshot of the current range state
*
* @param {ve.ce.RangeState|null} old Previous range state
* @param {ve.ce.BranchNode} root Surface root
* @param {boolean} selectionOnly The caller promises the content has not changed from old
*/
ve.ce.RangeState.prototype.saveState = function ( old, root, selectionOnly ) {
const oldSelection = old ? old.misleadingSelection : ve.SelectionState.static.newNullSelection(),
nativeSelection = root.getElementDocument().getSelection();
let selection;
if (
nativeSelection.rangeCount &&
OO.ui.contains( root.$element[ 0 ], nativeSelection.focusNode, true )
) {
// Freeze selection out of live object.
selection = new ve.SelectionState( nativeSelection );
} else {
// Use a blank selection if the selection is outside the document
selection = ve.SelectionState.static.newNullSelection();
}
// Get new range information
if ( selection.equalsSelection( oldSelection ) ) {
// No change; use old values for speed
this.selectionChanged = false;
this.veRange = old && old.veRange;
} else {
this.selectionChanged = true;
this.veRange = ve.ce.veRangeFromSelection( selection );
}
const focusNodeChanged = oldSelection.focusNode !== selection.focusNode;
if ( !focusNodeChanged ) {
this.node = old && old.node;
} else {
const $node = $( selection.focusNode ).closest( '.ve-ce-branchNode' );
if ( $node.length === 0 ) {
this.node = null;
} else {
this.node = $node.data( 'view' );
// Check this node belongs to our document
if ( this.node && this.node.root !== root.root ) {
this.node = null;
this.veRange = null;
}
}
}
this.branchNodeChanged = ( old && old.node ) !== this.node;
// Compute text/hash/textState, for change comparison
if ( !this.node ) {
this.text = null;
this.hash = null;
this.textState = null;
} else if ( selectionOnly && !focusNodeChanged ) {
this.text = old.text;
this.hash = old.hash;
this.textState = old.textState;
} else {
this.text = ve.ce.getDomText( this.node.$element[ 0 ] );
this.hash = ve.ce.getDomHash( this.node.$element[ 0 ] );
this.textState = new ve.ce.TextState( this.node.$element[ 0 ] );
}
// Only set contentChanged if we're still in the same branch node
this.contentChanged =
!selectionOnly &&
!this.branchNodeChanged && (
( old && old.hash ) !== this.hash ||
( old && old.text ) !== this.text ||
( !this.textState && old && old.textState ) ||
( !!this.textState && !this.textState.isEqual( old && old.textState ) )
);
if ( old && !this.selectionChanged && !this.contentChanged ) {
this.focusIsAfterAnnotationBoundary = old.focusIsAfterAnnotationBoundary;
} else {
// Will be null if there is no selection
this.focusIsAfterAnnotationBoundary = selection.focusNode &&
ve.ce.isAfterAnnotationBoundary(
selection.focusNode,
selection.focusOffset
);
}
// Save selection for future comparisons. (But it is not properly frozen, because the nodes
// are live and mutable, and therefore the offsets may come to point to places that are
// misleadingly different from when the selection was saved).
this.misleadingSelection = selection;
};