mediawiki-extensions-Visual.../modules/ve/ui/inspectors/ve.ui.AnnotationInspector.js

222 lines
6.5 KiB
JavaScript

/*!
* VisualEditor UserInterface AnnotationInspector class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Annotation inspector.
*
* @class
* @abstract
* @extends ve.ui.Inspector
*
* @constructor
* @param {ve.ui.WindowSet} windowSet Window set this inspector is part of
* @param {Object} [config] Configuration options
*/
ve.ui.AnnotationInspector = function VeUiAnnotationInspector( windowSet, config ) {
// Parent constructor
ve.ui.Inspector.call( this, windowSet, config );
// Properties
this.previousSelection = null;
this.initialSelection = null;
this.initialAnnotation = null;
this.initialAnnotationHash = null;
this.initialText = '';
this.isNewAnnotation = false;
};
/* Inheritance */
OO.inheritClass( ve.ui.AnnotationInspector, ve.ui.Inspector );
/**
* Annotation models this inspector can edit.
*
* @static
* @inheritable
* @property {Function[]}
*/
ve.ui.AnnotationInspector.static.modelClasses = [];
/* Methods */
/**
* 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
* @returns {ve.dm.Annotation} Annotation to apply
* @throws {Error} If not overridden in subclass
*/
ve.ui.AnnotationInspector.prototype.getAnnotation = function () {
throw new Error(
've.ui.AnnotationInspector.getAnnotation not implemented in subclass'
);
};
/**
* Get an annotation object from text.
*
* @method
* @abstract
* @param {string} text Content text
* @returns {ve.dm.Annotation}
* @throws {Error} If not overriden in a subclass
*/
ve.ui.AnnotationInspector.prototype.getAnnotationFromText = function () {
throw new Error(
've.ui.AnnotationInspector.getAnnotationFromText not implemented in subclass'
);
};
/**
* 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
* @returns {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 ( annnotation ) {
return ve.isInstanceOfAny( annnotation, modelClasses );
} );
};
/**
* 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
*/
ve.ui.AnnotationInspector.prototype.setup = function ( data ) {
// Parent method
ve.ui.Inspector.prototype.setup.call( this, data );
var expandedFragment, trimmedFragment, truncatedFragment,
fragment = this.surface.getModel().getFragment( null, true ),
annotation = this.getMatchingAnnotations( fragment, true ).get( 0 );
this.previousSelection = this.surface.getModel().getSelection();
this.initialText = '';
// Initialize range
if ( !annotation ) {
if ( fragment.getRange().isCollapsed() && !this.surface.view.hasSlugAtOffset( fragment.getRange().start ) ) {
// Expand to nearest word
expandedFragment = fragment.expandRange( 'word' );
fragment = expandedFragment;
} else {
// Trim whitespace
trimmedFragment = fragment.trimRange();
fragment = trimmedFragment;
}
if ( !fragment.getRange().isCollapsed() ) {
// Create annotation from selection
truncatedFragment = fragment.truncateRange( 255 );
fragment = truncatedFragment;
this.initialText = fragment.getText();
annotation = this.getAnnotationFromText( this.initialText );
if ( annotation ) {
fragment.annotateContent( 'set', annotation );
}
this.isNewAnnotation = true;
}
} else {
// Expand range to cover annotation
expandedFragment = fragment.expandRange( 'annotation', annotation );
fragment = expandedFragment;
}
// Update selection
fragment.select();
this.initialSelection = fragment.getRange();
// Note we don't set the 'all' flag here - any non-annotated content will be annotated on close
this.initialAnnotation = this.getMatchingAnnotations( fragment ).get( 0 );
this.initialAnnotationHash = this.initialAnnotation ?
OO.getHash( this.initialAnnotation.getComparableObject() ) : null;
};
/**
* @inheritdoc
*/
ve.ui.AnnotationInspector.prototype.teardown = function ( data ) {
// Configuration initialization
data = data || {};
var i, len, annotations,
insert = false,
undo = false,
clear = false,
set = false,
target = this.targetInput.getValue(),
annotation = this.getAnnotation(),
remove = target === '' || data.action === 'remove',
surfaceModel = this.surface.getModel(),
fragment = surfaceModel.getFragment( this.initialSelection, false ),
selection = surfaceModel.getSelection();
if ( remove ) {
clear = true;
} else if ( annotation ) {
if ( this.initialSelection.isCollapsed() ) {
insert = true;
}
if ( OO.getHash( annotation.getComparableObject() ) !== this.initialAnnotationHash ) {
if ( this.isNewAnnotation ) {
undo = true;
} else {
clear = true;
}
set = true;
}
}
if ( insert ) {
fragment.insertContent( target, false );
// Move cursor to the end of the inserted content, even if back button is used
this.previousSelection = new ve.Range( this.initialSelection.start + target.length );
}
if ( undo ) {
// Go back to before we added an annotation
this.surface.execute( 'history', 'undo' );
}
if ( clear ) {
// 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 ( set && annotation ) {
// Apply new annotation
fragment.annotateContent( 'set', annotation );
}
if ( data.action === 'back' || insert ) {
// Restore selection to what it was before we expanded it
selection = this.previousSelection;
}
this.surface.execute( 'content', 'select', selection );
// Reset state
this.isNewAnnotation = false;
// Parent method
ve.ui.Inspector.prototype.teardown.call( this, data );
};