mediawiki-extensions-Visual.../modules/ve/ui/inspectors/ve.ui.LinkInspector.js
Roan Kattouw 93b645a453 Make autonumbered external links inspectable
When the target of an autonumbered link is changed to a URL, it's kept
as an autonumbered link and its target is updated. When the target is
changed to a MediaWiki page, the autonumbered link is removed and
replaced with an internal link with the text set to the target.
So for instance, if you inspect [http://www.example.com] and change
its target to "Foo", the result will be [[Foo]].

The core of this commit adds support for inspecting nodes to
ve.ui.LinkInspector. This support should probably move into a
class in between AnnotationInspector and LinkInspector, perhaps
called HybridInspector or something, but I'm deferring that for now.

LinkInspector allows changes to inspected nodes to be reflected either
as attribute changes on the node, or by replacing the node with something
else. MWLinkInspector uses this feature to replace the autonumbered
external link node with an internal link annotation when the target is
set to an external link.

Bug: 53505
Change-Id: Icb404af84c24574438e4de3ef05bbd1993b593f7
2013-11-30 00:14:41 -08:00

199 lines
5.5 KiB
JavaScript

/*!
* VisualEditor UserInterface LinkInspector class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Link inspector.
*
* @class
* @extends ve.ui.AnnotationInspector
*
* @constructor
* @param {ve.ui.WindowSet} windowSet Window set this inspector is part of
* @param {Object} [config] Configuration options
*/
ve.ui.LinkInspector = function VeUiLinkInspector( windowSet, config ) {
// Parent constructor
ve.ui.AnnotationInspector.call( this, windowSet, config );
// Properties
this.linkNode = null;
};
/* Inheritance */
OO.inheritClass( ve.ui.LinkInspector, ve.ui.AnnotationInspector );
/* Static properties */
ve.ui.LinkInspector.static.name = 'link';
ve.ui.LinkInspector.static.icon = 'link';
ve.ui.LinkInspector.static.titleMessage = 'visualeditor-linkinspector-title';
ve.ui.LinkInspector.static.linkTargetInputWidget = ve.ui.LinkTargetInputWidget;
/**
* Models this inspector can edit.
*
* These models may be either annotations or nodes. When nodes are inspected, #getNodeChanges
* is used instead of #getAnnotation to determine what changes to save when the inspector is
* closed.
*
* @static
* @property {Function[]}
*/
ve.ui.LinkInspector.static.modelClasses = [ ve.dm.LinkAnnotation ];
/* Methods */
/**
* Get the annotation from the input.
*
* This override allows AnnotationInspector to request the value from the inspector rather
* than the widget.
*
* @method
* @returns {ve.dm.LinkAnnotation} Link annotation
*/
ve.ui.LinkInspector.prototype.getAnnotation = function () {
return this.targetInput.annotation;
};
/**
* @inheritdoc
*/
ve.ui.LinkInspector.prototype.getAnnotationFromText = function ( text ) {
return new ve.dm.LinkAnnotation( { 'type': 'link', 'attributes': { 'href': text } } );
};
/**
* Get the changes to make to the node that's currently being inspected.
*
* This function can either return a plain object with attribute changes to make to the node,
* or an array with linear model data to replace the node with.
*
* This function will only be invoked if this.linkNode is set.
*
* @returns {Object|Array} Object with attribute changes, or linear model array
*/
ve.ui.LinkInspector.prototype.getNodeChanges = function () {
return { 'href': this.targetInput.annotation.getAttribute( 'href' ) };
};
/**
* @inheritdoc
*/
ve.ui.LinkInspector.prototype.initialize = function () {
// Parent method
ve.ui.AnnotationInspector.prototype.initialize.call( this );
// Properties
this.targetInput = new this.constructor.static.linkTargetInputWidget( {
'$': this.$, '$overlay': this.surface.$localOverlayMenus
} );
// Initialization
this.$form.append( this.targetInput.$element );
};
/**
* @inheritdoc
*/
ve.ui.LinkInspector.prototype.setup = function ( data ) {
var focusedNode = this.surface.getView().getFocusedNode();
if ( focusedNode && ve.isInstanceOfAny( focusedNode.getModel(), this.constructor.static.modelClasses ) ) {
this.linkNode = focusedNode.getModel();
// Call grandparent method, skipping AnnotationInspector
ve.ui.Inspector.prototype.setup.call( this, data );
} else {
this.linkNode = null;
// Parent method
ve.ui.AnnotationInspector.prototype.setup.call( this, data );
}
// Disable surface until animation is complete; will be reenabled in ready()
this.surface.disable();
};
/**
* @inheritdoc
*/
ve.ui.LinkInspector.prototype.ready = function () {
// Parent method
ve.ui.AnnotationInspector.prototype.ready.call( this );
// Note: Focus input prior to setting target annotation
this.targetInput.$input.focus();
// Setup targetInput contents
if ( this.linkNode ) {
// FIXME this assumes the linkNode will always have an href attribute
this.targetInput.setValue( this.linkNode.getAttribute( 'href' ) );
} else if ( this.initialAnnotation ) {
this.targetInput.setAnnotation( this.initialAnnotation );
} else {
// If an initial annotation couldn't be created (e.g. the text was invalid),
// just populate the text we tried to create the annotation from
this.targetInput.setValue( this.initialText );
}
this.targetInput.$input.select();
this.surface.enable();
};
/**
* @inheritdoc
*/
ve.ui.LinkInspector.prototype.teardown = function ( data ) {
var changes, remove, replace, nodeRange, surfaceModel = this.surface.getModel();
if ( this.linkNode ) {
nodeRange = this.linkNode.getOuterRange();
changes = this.getNodeChanges();
replace = ve.isArray( changes );
// FIXME figure out a better way to do the "if input is empty, remove" thing,
// not duplicating it here from AnnotationInspector (where it doesn't even belong)
remove = data.action === 'remove' || this.targetInput.getValue() === '';
if ( remove || replace ) {
surfaceModel.change(
ve.dm.Transaction.newFromRemoval(
surfaceModel.getDocument(),
nodeRange
)
);
}
if ( !remove ) {
if ( replace ) {
// We've already removed the node, so we just need to do an insertion now
surfaceModel.change(
ve.dm.Transaction.newFromInsertion(
surfaceModel.getDocument(),
nodeRange.start,
changes
)
);
} else {
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChanges(
surfaceModel.getDocument(),
nodeRange.start,
changes
)
);
}
}
// Call grandparent method, skipping AnnotationInspector
ve.ui.Inspector.prototype.teardown.call( this, data );
} else {
// Parent method
ve.ui.AnnotationInspector.prototype.teardown.call( this, data );
}
};
/* Registration */
ve.ui.inspectorFactory.register( ve.ui.LinkInspector );