2013-07-03 20:25:06 +00:00
|
|
|
/*!
|
|
|
|
* 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
|
2013-10-04 17:51:44 +00:00
|
|
|
* @extends ve.ui.SurfaceInspector
|
2013-07-03 20:25:06 +00:00
|
|
|
*
|
|
|
|
* @constructor
|
2013-10-09 19:59:03 +00:00
|
|
|
* @param {ve.ui.SurfaceWindowSet} windowSet Window set this inspector is part of
|
2013-09-25 10:21:09 +00:00
|
|
|
* @param {Object} [config] Configuration options
|
2013-07-03 20:25:06 +00:00
|
|
|
*/
|
2013-10-09 19:59:03 +00:00
|
|
|
ve.ui.AnnotationInspector = function VeUiAnnotationInspector( windowSet, config ) {
|
2013-07-03 20:25:06 +00:00
|
|
|
// Parent constructor
|
2013-10-09 19:59:03 +00:00
|
|
|
ve.ui.SurfaceInspector.call( this, windowSet, config );
|
2013-07-03 20:25:06 +00:00
|
|
|
|
|
|
|
// Properties
|
|
|
|
this.initialAnnotation = null;
|
|
|
|
this.initialAnnotationHash = null;
|
2013-08-01 11:53:11 +00:00
|
|
|
this.initialText = null;
|
2013-07-03 20:25:06 +00:00
|
|
|
this.isNewAnnotation = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Inheritance */
|
|
|
|
|
2013-10-11 21:44:09 +00:00
|
|
|
OO.inheritClass( ve.ui.AnnotationInspector, ve.ui.SurfaceInspector );
|
2013-07-03 20:25:06 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Annotation models this inspector can edit.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @inheritable
|
|
|
|
* @property {Function[]}
|
|
|
|
*/
|
|
|
|
ve.ui.AnnotationInspector.static.modelClasses = [];
|
|
|
|
|
|
|
|
/* Methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
ve.ui.AnnotationInspector.prototype.onSetup = function () {
|
|
|
|
var expandedFragment, trimmedFragment, truncatedFragment,
|
|
|
|
fragment = this.surface.getModel().getFragment( null, true ),
|
|
|
|
annotation = this.getMatchingAnnotations( fragment, true ).get( 0 );
|
|
|
|
|
|
|
|
// Parent method
|
2013-10-04 17:51:44 +00:00
|
|
|
ve.ui.SurfaceInspector.prototype.onSetup.call( this );
|
2013-07-03 20:25:06 +00:00
|
|
|
// Initialize range
|
|
|
|
if ( !annotation ) {
|
2013-08-27 23:45:38 +00:00
|
|
|
if ( fragment.getRange().isCollapsed() && !this.surface.view.hasSlugAtOffset( fragment.getRange().start ) ) {
|
2013-07-03 20:25:06 +00:00
|
|
|
// 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;
|
2013-08-01 11:53:11 +00:00
|
|
|
this.initialText = fragment.getText();
|
|
|
|
annotation = this.getAnnotationFromText( this.initialText );
|
|
|
|
if ( annotation ) {
|
|
|
|
fragment.annotateContent( 'set', annotation );
|
|
|
|
}
|
2013-07-03 20:25:06 +00:00
|
|
|
this.isNewAnnotation = true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Expand range to cover annotation
|
|
|
|
expandedFragment = fragment.expandRange( 'annotation', annotation );
|
|
|
|
fragment = expandedFragment;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update selection
|
|
|
|
fragment.select();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle the inspector being opened.
|
|
|
|
*/
|
|
|
|
ve.ui.AnnotationInspector.prototype.onOpen = function () {
|
|
|
|
var fragment = this.surface.getModel().getFragment( null, true ),
|
|
|
|
// Note that we don't set the 'all' flag here so that any
|
|
|
|
// non-annotated content is annotated on close
|
|
|
|
initialAnnotation = this.getMatchingAnnotations( fragment ).get( 0 );
|
|
|
|
|
|
|
|
// Parent method
|
2013-10-04 17:51:44 +00:00
|
|
|
ve.ui.SurfaceInspector.prototype.onOpen.call( this );
|
2013-07-03 20:25:06 +00:00
|
|
|
|
|
|
|
// Initialization
|
|
|
|
this.initialAnnotation = initialAnnotation;
|
2013-10-15 19:59:14 +00:00
|
|
|
this.initialAnnotationHash = initialAnnotation ? OO.getHash( initialAnnotation ) : null;
|
2013-07-03 20:25:06 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle the inspector being closed.
|
|
|
|
*
|
|
|
|
* @param {string} action Action that caused the window to be closed
|
|
|
|
*/
|
|
|
|
ve.ui.AnnotationInspector.prototype.onClose = function ( action ) {
|
|
|
|
// Parent method
|
2013-10-04 17:51:44 +00:00
|
|
|
ve.ui.SurfaceInspector.prototype.onClose.call( this, action );
|
2013-07-03 20:25:06 +00:00
|
|
|
|
2013-07-30 22:06:35 +00:00
|
|
|
var i, len, annotations,
|
2013-07-03 20:25:06 +00:00
|
|
|
insert = false,
|
|
|
|
undo = false,
|
|
|
|
clear = false,
|
|
|
|
set = false,
|
|
|
|
target = this.targetInput.getValue(),
|
2013-08-10 09:27:20 +00:00
|
|
|
annotation = this.getAnnotation(),
|
2013-07-30 22:06:35 +00:00
|
|
|
remove = target === '' || ( action === 'remove' && !!annotation ),
|
2013-07-09 23:09:57 +00:00
|
|
|
surfaceModel = this.surface.getModel(),
|
|
|
|
fragment = surfaceModel.getFragment( this.initialSelection, false ),
|
2013-07-30 22:06:35 +00:00
|
|
|
selection = surfaceModel.getSelection();
|
2013-07-03 20:25:06 +00:00
|
|
|
|
|
|
|
if ( remove ) {
|
|
|
|
clear = true;
|
2013-08-01 11:53:11 +00:00
|
|
|
} else if ( annotation ) {
|
2013-07-03 20:25:06 +00:00
|
|
|
if ( this.initialSelection.isCollapsed() ) {
|
|
|
|
insert = true;
|
|
|
|
}
|
2013-10-15 19:59:14 +00:00
|
|
|
if ( OO.getHash( annotation ) !== this.initialAnnotationHash ) {
|
2013-07-03 20:25:06 +00:00
|
|
|
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] );
|
|
|
|
}
|
|
|
|
}
|
2013-07-30 22:06:35 +00:00
|
|
|
if ( set && annotation ) {
|
2013-07-03 20:25:06 +00:00
|
|
|
// Apply new annotation
|
|
|
|
fragment.annotateContent( 'set', annotation );
|
|
|
|
}
|
2013-08-29 21:42:18 +00:00
|
|
|
if ( action === 'back' || insert ) {
|
2013-07-30 22:06:35 +00:00
|
|
|
// Restore selection to what it was before we expanded it
|
2013-07-03 20:25:06 +00:00
|
|
|
selection = this.previousSelection;
|
|
|
|
}
|
2013-07-30 22:06:35 +00:00
|
|
|
this.surface.execute( 'content', 'select', selection );
|
2013-07-03 20:25:06 +00:00
|
|
|
// Reset state
|
|
|
|
this.isNewAnnotation = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 );
|
|
|
|
} );
|
|
|
|
};
|