Current File : /home/jvzmxxx/wiki1/extensions/VisualEditor/lib/ve/src/ui/inspectors/ve.ui.AnnotationInspector.js
/*!
 * VisualEditor UserInterface AnnotationInspector class.
 *
 * @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org
 */

/**
 * Inspector for working with content annotations.
 *
 * @class
 * @abstract
 * @extends ve.ui.FragmentInspector
 *
 * @constructor
 * @param {Object} [config] Configuration options
 */
ve.ui.AnnotationInspector = function VeUiAnnotationInspector() {
	// Parent constructor
	ve.ui.AnnotationInspector.super.apply( this, arguments );

	// Properties
	this.initialSelection = null;
	this.initialAnnotation = null;
	this.initialAnnotationIsCovering = false;
};

/* Inheritance */

OO.inheritClass( ve.ui.AnnotationInspector, ve.ui.FragmentInspector );

/**
 * Annotation models this inspector can edit.
 *
 * @static
 * @inheritable
 * @property {Function[]}
 */
ve.ui.AnnotationInspector.static.modelClasses = [];

// Override the parent action array to only have a 'cancel' button
// on insert, since the annotation inspectors immediately apply the
// action and 'cancel' is meaningless. Instead, they use 'done' to
// perform the same dismissal after applying action that clicking away
// from the inspector performs.
ve.ui.AnnotationInspector.static.actions = [
	{
		action: 'done',
		label: OO.ui.deferMsg( 'visualeditor-dialog-action-done' ),
		flags: [ 'progressive', 'primary' ],
		modes: 'edit'
	},
	{
		label: OO.ui.deferMsg( 'visualeditor-dialog-action-cancel' ),
		flags: [ 'safe', 'back' ],
		modes: [ 'edit', 'insert' ]
	},
	{
		action: 'done',
		label: OO.ui.deferMsg( 'visualeditor-dialog-action-insert' ),
		flags: [ 'constructive', 'primary' ],
		modes: 'insert'
	}
];

/* Methods */

/**
 * Check if form is empty, which if saved should result in removing the annotation.
 *
 * Only override this if the form provides the user a way to blank out primary information, allowing
 * them to remove the annotation by clearing the form.
 *
 * @return {boolean} Form is empty
 */
ve.ui.AnnotationInspector.prototype.shouldRemoveAnnotation = function () {
	return false;
};

/**
 * Get data to insert if nothing was selected when the inspector opened.
 *
 * Defaults to using #getInsertionText.
 *
 * @return {Array} Linear model content to insert
 */
ve.ui.AnnotationInspector.prototype.getInsertionData = function () {
	return this.getInsertionText().split( '' );
};

/**
 * Get text to insert if nothing was selected when the inspector opened.
 *
 * @return {string} Text to insert
 */
ve.ui.AnnotationInspector.prototype.getInsertionText = function () {
	return '';
};

/**
 * Get the annotation object to apply.
 *
 * This method is called when the inspector is closing, and should return the annotation to apply
 * to the text. If this method returns a falsey value like null, no annotation will be applied,
 * but existing annotations won't be removed either.
 *
 * @abstract
 * @method
 * @return {ve.dm.Annotation} Annotation to apply
 */
ve.ui.AnnotationInspector.prototype.getAnnotation = null;

/**
 * Get an annotation object from a fragment.
 *
 * @abstract
 * @method
 * @param {ve.dm.SurfaceFragment} fragment Surface fragment
 * @return {ve.dm.Annotation|null} Annotation
 */
ve.ui.AnnotationInspector.prototype.getAnnotationFromFragment = null;

/**
 * Get matching annotations within a fragment.
 *
 * @method
 * @param {ve.dm.SurfaceFragment} fragment Fragment to get matching annotations within
 * @param {boolean} [all] Get annotations which only cover some of the fragment
 * @return {ve.dm.AnnotationSet} Matching annotations
 */
ve.ui.AnnotationInspector.prototype.getMatchingAnnotations = function ( fragment, all ) {
	var modelClasses = this.constructor.static.modelClasses;

	return fragment.getAnnotations( all ).filter( function ( annotation ) {
		return ve.isInstanceOfAny( annotation, modelClasses );
	} );
};

/**
 * @inheritdoc
 */
ve.ui.AnnotationInspector.prototype.getMode = function () {
	if ( this.initialSelection ) {
		return this.initialSelection.isCollapsed() ? 'insert' : 'edit';
	}
	return '';
};

/**
 * Handle the inspector being setup.
 *
 * There are 4 scenarios:
 *
 * - Zero-length selection not near a word -> no change, text will be inserted on close
 * - Zero-length selection inside or adjacent to a word -> expand selection to cover word
 * - Selection covering non-annotated text -> trim selection to remove leading/trailing whitespace
 * - Selection covering annotated text -> expand selection to cover annotation
 *
 * @method
 * @param {Object} [data] Inspector opening data
 * @param {boolean} [data.noExpand] Don't expand the selection when opening
 */
ve.ui.AnnotationInspector.prototype.getSetupProcess = function ( data ) {
	return ve.ui.AnnotationInspector.super.prototype.getSetupProcess.call( this, data )
		.next( function () {
			var initialCoveringAnnotation,
				inspector = this,
				annotationSet, annotations,
				fragment = this.getFragment(),
				surfaceModel = fragment.getSurface(),
				annotation = this.getMatchingAnnotations( fragment, true ).get( 0 );

			surfaceModel.pushStaging();

			// Initialize range
			if ( this.previousSelection instanceof ve.dm.LinearSelection && !annotation ) {
				if (
					fragment.getSelection().isCollapsed() &&
					fragment.getDocument().data.isContentOffset( fragment.getSelection().getRange().start )
				) {
					// Expand to nearest word
					if ( !data.noExpand ) {
						fragment = fragment.expandLinearSelection( 'word' );
					}

					// TODO: We should review how getMatchingAnnotation works in light of the fact
					// that in the case of a collapsed range, the method falls back to retrieving
					// insertion annotations.

					// Check if we're inside a relevant annotation and if so, define it
					annotationSet = fragment.document.data.getAnnotationsFromRange( fragment.selection.range );
					annotations = annotationSet.filter( function ( existingAnnotation ) {
						return ve.isInstanceOfAny( existingAnnotation, inspector.constructor.static.modelClasses );
					} );
					if ( annotations.getLength() > 0 ) {
						// We're in the middle of an annotation, let's make sure we expand
						// our selection to include the entire existing annotation
						annotation = annotations.get( 0 );
					}
				} else {
					// Trim whitespace
					fragment = fragment.trimLinearSelection();
				}

				if ( !fragment.getSelection().isCollapsed() && !annotation ) {
					// Create annotation from selection
					annotation = this.getAnnotationFromFragment( fragment );
					if ( annotation ) {
						fragment.annotateContent( 'set', annotation );
					}
				}
			}
			if ( annotation && !data.noExpand ) {
				// Expand range to cover annotation
				fragment = fragment.expandLinearSelection( 'annotation', annotation );
			}

			// Update selection
			fragment.select();
			this.initialSelection = fragment.getSelection();

			// The initial annotation is the first matching annotation in the fragment
			this.initialAnnotation = this.getMatchingAnnotations( fragment, true ).get( 0 );
			initialCoveringAnnotation = this.getMatchingAnnotations( fragment ).get( 0 );
			// Fallback to a default annotation
			if ( !this.initialAnnotation ) {
				this.initialAnnotation = this.getAnnotationFromFragment( fragment );
			} else if (
				initialCoveringAnnotation &&
				initialCoveringAnnotation.compareTo( this.initialAnnotation )
			) {
				// If the initial annotation doesn't cover the fragment, record this as we'll need
				// to forcefully apply it to the rest of the fragment later
				this.initialAnnotationIsCovering = true;
			}

			this.fragment = fragment;

			// Set the mode - this was done already in FragmentInspector but now that we may have
			// changed what the fragment is covering we need to run it again
			this.actions.setMode( this.getMode() );
		}, this );
};

/**
 * @inheritdoc
 */
ve.ui.AnnotationInspector.prototype.getTeardownProcess = function ( data ) {
	data = data || {};
	return ve.ui.AnnotationInspector.super.prototype.getTeardownProcess.call( this, data )
		.first( function () {
			var i, len, annotations, insertion,
				insertionAnnotation = false,
				insertText = false,
				replace = false,
				annotation = this.getAnnotation(),
				remove = data.action === 'done' && this.shouldRemoveAnnotation(),
				surfaceModel = this.fragment.getSurface(),
				fragment = surfaceModel.getFragment( this.initialSelection, false ),
				selection = this.fragment.getSelection();

			if (
				!( selection instanceof ve.dm.LinearSelection ) ||
				( remove && selection.getRange().isCollapsed() )
			) {
				// Since we pushStaging on SetupProcess we need to make sure
				// all terminations pop
				surfaceModel.popStaging();
				return;
			}

			if ( !remove ) {
				if ( data.action !== 'done' ) {
					surfaceModel.popStaging();
					if ( this.previousSelection ) {
						surfaceModel.setSelection( this.previousSelection );
					}
					return;
				}
				if ( this.initialSelection.isCollapsed() ) {
					insertText = true;
				}
				if ( annotation ) {
					// Check if the initial annotation has changed, or didn't cover the whole fragment
					// to begin with
					if (
						!this.initialAnnotationIsCovering ||
						!this.initialAnnotation ||
						!this.initialAnnotation.compareTo( annotation )
					) {
						replace = true;
					}
				}
			}
			// If we are setting a new annotation, clear any annotations the inspector may have
			// applied up to this point. Otherwise keep them.
			if ( replace ) {
				surfaceModel.popStaging();
			} else {
				surfaceModel.applyStaging();
			}
			if ( insertText ) {
				insertion = this.getInsertionData();
				if ( insertion.length ) {
					fragment.insertContent( insertion, true );
					// Move cursor to the end of the inserted content, even if back button is used
					fragment.adjustLinearSelection( -insertion.length, 0 );
					this.previousSelection = new ve.dm.LinearSelection( fragment.getDocument(), new ve.Range(
						this.initialSelection.getRange().start + insertion.length
					) );
				}
			}
			if ( remove || replace ) {
				// Clear all existing annotations
				annotations = this.getMatchingAnnotations( fragment, true ).get();
				for ( i = 0, len = annotations.length; i < len; i++ ) {
					fragment.annotateContent( 'clear', annotations[ i ] );
				}
			}
			if ( replace ) {
				// Apply new annotation
				if ( fragment.getSelection().isCollapsed() ) {
					insertionAnnotation = true;
				} else {
					fragment.annotateContent( 'set', annotation );
				}
			}
			if ( !data.action || insertText ) {
				// Restore selection to what it was before we expanded it
				selection = this.previousSelection;
			}
			if ( data.action ) {
				surfaceModel.setSelection( selection );
			}

			if ( insertionAnnotation ) {
				surfaceModel.addInsertionAnnotations( annotation );
			}
		}, this )
		.next( function () {
			// Reset state
			this.initialSelection = null;
			this.initialAnnotation = null;
			this.initialAnnotationIsCovering = false;
		}, this );
};