mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 08:34:54 +00:00
222 lines
6.5 KiB
JavaScript
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 );
|
|
};
|