mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 00:30:44 +00:00
b1d9c83b5d
* For the most common case: - replace ve.extendClass with ve.inheritClass (chose slightly different names to detect usage of the old/new one, and I like 'inherit' better). - move it up to below the constructor, see doc block for why. * Cases where more than 2 arguments were passed to ve.extendClass are handled differently depending on the case. In case of a longer inheritance tree, the other arguments could be omitted (like in "ve.ce.FooBar, ve.FooBar, ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar, because ve.ce.FooBar inherits from ve.Bar). In the case of where it previously had two mixins with ve.extendClass(), either one becomes inheritClass and one a mixin, both to mixinClass(). No visible changes should come from this commit as the instances still all have the same visible properties in the end. No more or less than before. * Misc.: - Be consistent in calling parent constructors in the same order as the inheritance. - Add missing @extends and @param documentation. - Replace invalid {Integer} type hint with {Number}. - Consistent doc comments order: @class, @abstract, @constructor, @extends, @params. - Fix indentation errors A fairly common mistake was a superfluous space before the identifier on the assignment line directly below the documentation comment. $ ack "^ [^*]" --js modules/ve - Typo "Inhertiance" -> "Inheritance". - Replacing the other confusing comment "Inheritance" (inside the constructor) with "Parent constructor". - Add missing @abstract for ve.ui.Tool. - Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js - Add function names to all @constructor functions. Now that we have inheritance it is important and useful to have these functions not be anonymous. Example of debug shot: http://cl.ly/image/1j3c160w3D45 Makes the difference between < documentNode; > ve_dm_DocumentNode ... : ve_dm_BranchNode ... : ve_dm_Node ... : ve_dm_Node ... : Object ... without names (current situation): < documentNode; > Object ... : Object ... : Object ... : Object ... : Object ... though before this commit, it really looks like this (flattened since ve.extendClass really did a mixin): < documentNode; > Object ... ... ... Pattern in Sublime (case-sensitive) to find nameless constructor functions: "^ve\..*\.([A-Z])([^\.]+) = function \(" Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
364 lines
10 KiB
JavaScript
364 lines
10 KiB
JavaScript
/*global mw */
|
|
|
|
/**
|
|
* VisualEditor user interface LinkInspector class.
|
|
*
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* Creates an ve.ui.LinkInspector object.
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @extends {ve.ui.Inspector}
|
|
* @param {ve.ui.Toolbar} toolbar
|
|
* @param context
|
|
*/
|
|
ve.ui.LinkInspector = function ve_ui_LinkInspector( toolbar, context ) {
|
|
var inspector = this;
|
|
|
|
// Inheritance
|
|
ve.ui.Inspector.call( this, toolbar, context );
|
|
|
|
// Properties
|
|
this.context = context;
|
|
this.initialValue = null;
|
|
this.$clearButton = $(
|
|
'<div class="ve-ui-inspector-button ve-ui-inspector-clearButton"></div>',
|
|
context.inspectorDoc
|
|
);
|
|
this.$title = $( '<div class="ve-ui-inspector-title"></div>', context.inspectorDoc )
|
|
.text( ve.msg( 'visualeditor-linkinspector-title' ) );
|
|
this.$locationInput = $(
|
|
'<input type="text" class="ve-ui-linkInspector-location" />',
|
|
context.inspectorDoc
|
|
);
|
|
|
|
// Events
|
|
this.$clearButton.click( function () {
|
|
if ( $(this).is( '.ve-ui-inspector-button-disabled' ) ) {
|
|
return;
|
|
}
|
|
var i, arr,
|
|
surfaceModel = inspector.context.getSurfaceView().getModel(),
|
|
annotations = inspector.getAllLinkAnnotationsFromSelection();
|
|
// Clear all link annotations.
|
|
arr = annotations.get();
|
|
for ( i = 0; i < arr.length; i++ ) {
|
|
surfaceModel.annotate( 'clear', arr[i] );
|
|
}
|
|
inspector.$locationInput.val( '' );
|
|
inspector.context.closeInspector();
|
|
} );
|
|
this.$locationInput.on( 'change mousedown keydown cut paste', function () {
|
|
setTimeout( function () {
|
|
// Toggle disabled class
|
|
if ( inspector.$locationInput.val() !== '' ) {
|
|
inspector.$acceptButton.removeClass( 've-ui-inspector-button-disabled' );
|
|
} else {
|
|
inspector.$acceptButton.addClass( 've-ui-inspector-button-disabled' );
|
|
}
|
|
|
|
}, 0 );
|
|
} );
|
|
|
|
// DOM Changes
|
|
this.$.prepend( this.$title, this.$clearButton );
|
|
this.$form.append( this.$locationInput );
|
|
|
|
// FIXME: MediaWiki-specific
|
|
if ( 'mw' in window ) {
|
|
this.initMultiSuggest();
|
|
}
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
ve.inheritClass( ve.ui.LinkInspector, ve.ui.Inspector );
|
|
|
|
/* Methods */
|
|
|
|
ve.ui.LinkInspector.prototype.getAllLinkAnnotationsFromSelection = function () {
|
|
var surfaceView = this.context.getSurfaceView(),
|
|
surfaceModel = surfaceView.getModel(),
|
|
documentModel = surfaceModel.getDocument();
|
|
return documentModel
|
|
.getAnnotationsFromRange( surfaceModel.getSelection() )
|
|
.getAnnotationsOfType( /^link\// );
|
|
};
|
|
|
|
ve.ui.LinkInspector.prototype.getFirstLinkAnnotation = function ( annotations ) {
|
|
var i, annotation, arr = annotations.get();
|
|
for ( i = 0; i < arr.length; i++ ) {
|
|
// Use the first one with a recognized type (there should only be one, this is just in case)
|
|
annotation = arr[i];
|
|
if (
|
|
annotation.type === 'link/WikiLink' ||
|
|
annotation.type === 'link/ExtLink' ||
|
|
annotation.type === 'link/ExtLink/Numbered' ||
|
|
annotation.type === 'link/ExtLink/URL'
|
|
) {
|
|
return annotation;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// TODO: This should probably be somewhere else but I needed this here for now.
|
|
ve.ui.LinkInspector.prototype.getSelectionText = function () {
|
|
var i,
|
|
surfaceView = this.context.getSurfaceView(),
|
|
surfaceModel = surfaceView.getModel(),
|
|
documentModel = surfaceModel.getDocument(),
|
|
data = documentModel.getData( surfaceModel.getSelection() ),
|
|
str = '',
|
|
max = Math.min( data.length, 255 );
|
|
for ( i = 0; i < max; i++ ) {
|
|
if ( ve.isArray( data[i] ) ) {
|
|
str += data[i][0];
|
|
} else if ( typeof data[i] === 'string' ) {
|
|
str += data[i];
|
|
}
|
|
}
|
|
return str;
|
|
};
|
|
|
|
/*
|
|
* Method called prior to opening inspector which fixes up
|
|
* selection to contain the complete annotated link range
|
|
* OR unwrap outer whitespace from selection.
|
|
*/
|
|
ve.ui.LinkInspector.prototype.prepareOpen = function () {
|
|
var surfaceView = this.context.getSurfaceView(),
|
|
surfaceModel = surfaceView.getModel(),
|
|
doc = surfaceModel.getDocument(),
|
|
annotations = this.getAllLinkAnnotationsFromSelection(),
|
|
annotation = this.getFirstLinkAnnotation( annotations ),
|
|
selection = surfaceModel.getSelection(),
|
|
annotatedRange,
|
|
newSelection;
|
|
|
|
// Trim outer space from range if any.
|
|
newSelection = doc.trimOuterSpaceFromRange( selection );
|
|
|
|
if ( annotation !== null ) {
|
|
annotatedRange = doc.getAnnotatedRangeFromSelection( newSelection, annotation );
|
|
|
|
// Adjust selection if it does not contain the annotated range
|
|
if ( selection.start > annotatedRange.start ||
|
|
selection.end < annotatedRange.end
|
|
) {
|
|
newSelection = annotatedRange;
|
|
// if selected from right to left
|
|
if ( selection.from > selection.start ) {
|
|
newSelection.flip();
|
|
}
|
|
}
|
|
}
|
|
surfaceModel.change( null, newSelection );
|
|
};
|
|
|
|
ve.ui.LinkInspector.prototype.onOpen = function () {
|
|
var annotation = this.getFirstLinkAnnotation( this.getAllLinkAnnotationsFromSelection() ),
|
|
initialValue = '';
|
|
if ( annotation === null ) {
|
|
this.$locationInput.val( this.getSelectionText() );
|
|
this.$clearButton.addClass( 've-ui-inspector-button-disabled' );
|
|
} else if ( annotation.type === 'link/WikiLink' ) {
|
|
// Internal link
|
|
initialValue = annotation.data.title || '';
|
|
this.$locationInput.val( initialValue );
|
|
this.$clearButton.removeClass( 've-ui-inspector-button-disabled' );
|
|
} else {
|
|
// External link
|
|
initialValue = annotation.data.href || '';
|
|
this.$locationInput.val( initialValue );
|
|
this.$clearButton.removeClass( 've-ui-inspector-button-disabled' );
|
|
}
|
|
this.initialValue = initialValue;
|
|
if ( this.$locationInput.val().length === 0 ) {
|
|
this.$acceptButton.addClass( 've-ui-inspector-button-disabled' );
|
|
} else {
|
|
this.$acceptButton.removeClass( 've-ui-inspector-button-disabled' );
|
|
}
|
|
|
|
setTimeout( ve.bind( function () {
|
|
this.$locationInput.focus().select();
|
|
}, this ), 0 );
|
|
};
|
|
|
|
ve.ui.LinkInspector.prototype.onClose = function ( accept ) {
|
|
var surfaceView = this.context.getSurfaceView(),
|
|
surfaceModel = surfaceView.getModel(),
|
|
annotations = this.getAllLinkAnnotationsFromSelection(),
|
|
target = this.$locationInput.val(),
|
|
i, annotation, arr;
|
|
if ( accept ) {
|
|
if ( !target ) {
|
|
return;
|
|
}
|
|
// Clear link annotation if it exists
|
|
arr = annotations.get();
|
|
for ( i = 0; i < arr.length; i++ ) {
|
|
surfaceModel.annotate( 'clear', arr[i] );
|
|
}
|
|
surfaceModel.annotate( 'set', ve.ui.LinkInspector.getAnnotationForTarget( target ) );
|
|
|
|
}
|
|
// Restore focus
|
|
surfaceView.getDocument().getDocumentNode().$.focus();
|
|
};
|
|
|
|
ve.ui.LinkInspector.getAnnotationForTarget = function ( target ) {
|
|
var title;
|
|
// Figure out if this is an internal or external link
|
|
if ( target.match( /^(https?:)?\/\// ) ) {
|
|
// External link
|
|
return {
|
|
'type': 'link/ExtLink',
|
|
'data': { 'href': target }
|
|
};
|
|
} else {
|
|
// Internal link
|
|
// TODO: In the longer term we'll want to have autocompletion and existence
|
|
// and validity checks using AJAX
|
|
try {
|
|
// FIXME: MediaWiki-specific
|
|
title = new mw.Title( target );
|
|
if ( title.getNamespaceId() === 6 || title.getNamespaceId() === 14 ) {
|
|
// File: or Category: link
|
|
// We have to prepend a colon so this is interpreted as a link
|
|
// rather than an image inclusion or categorization
|
|
target = ':' + target;
|
|
}
|
|
} catch ( e ) { }
|
|
|
|
return {
|
|
'type': 'link/WikiLink',
|
|
'data': { 'title': target }
|
|
};
|
|
}
|
|
};
|
|
|
|
ve.ui.LinkInspector.prototype.initMultiSuggest = function () {
|
|
var inspector = this,
|
|
context = inspector.context,
|
|
$overlay = context.$iframeOverlay,
|
|
cache = {},
|
|
options;
|
|
|
|
// Multi Suggest configuration.
|
|
options = {
|
|
'parent': $overlay,
|
|
'prefix': 've-ui',
|
|
// Disable CSS Ellipsis.
|
|
// Using MediaWiki jQuery.autoEllipsis() for center ellipsis.
|
|
'cssEllipsis': false,
|
|
// Build suggestion groups in order.
|
|
'suggestions': function ( params ) {
|
|
var groups = {},
|
|
results = params.results,
|
|
query = params.query,
|
|
modifiedQuery,
|
|
title,
|
|
prot;
|
|
|
|
// Add existing pages.
|
|
if ( results.length > 0 ) {
|
|
groups.existingPage = {
|
|
'label': ve.msg( 'visualeditor-linkinspector-suggest-existing-page' ),
|
|
'items': results,
|
|
'itemClass': 've-ui-suggest-item-existingPage'
|
|
};
|
|
}
|
|
// Run the query through the mw.Title object to handle correct capitalization.
|
|
try {
|
|
title = new mw.Title( query );
|
|
modifiedQuery = title.getPrefixedText();
|
|
// If page doesn't exist, add New Page group.
|
|
if ( ve.indexOf( modifiedQuery, results ) === -1 ) {
|
|
groups.newPage = {
|
|
'label': ve.msg( 'visualeditor-linkinspector-suggest-new-page' ),
|
|
'items': [modifiedQuery],
|
|
'itemClass': 've-ui-suggest-item-newPage'
|
|
};
|
|
}
|
|
} catch ( e ) {
|
|
// invalid input
|
|
ve.log( e );
|
|
}
|
|
// Add external
|
|
groups.externalLink = {
|
|
'label': ve.msg( 'visualeditor-linkinspector-suggest-external-link' ),
|
|
'items': [],
|
|
'itemClass': 've-ui-suggest-item-externalLink'
|
|
};
|
|
// Find a protocol and suggest an external link.
|
|
prot = query.match(
|
|
ve.init.platform.getExternalLinkUrlProtocolsRegExp()
|
|
);
|
|
if ( prot ) {
|
|
groups.externalLink.items = [query];
|
|
// No protocol, default to http
|
|
} else {
|
|
groups.externalLink.items = ['http://' + query];
|
|
}
|
|
return groups;
|
|
},
|
|
// Called on succesfull input.
|
|
'input': function ( callback ) {
|
|
var $input = $( this ),
|
|
query = $input.val(),
|
|
cKey = query.toLowerCase(),
|
|
api = null;
|
|
|
|
// Set overlay position.
|
|
options.position();
|
|
// Build from cache.
|
|
if ( cache[cKey] !== undefined ) {
|
|
callback( {
|
|
'query': query,
|
|
'results': cache[cKey]
|
|
} );
|
|
} else {
|
|
// No cache, build fresh.
|
|
api = new mw.Api();
|
|
// MW api request.
|
|
api.get( {
|
|
'action': 'opensearch',
|
|
'search': query
|
|
}, {
|
|
'ok': function ( data ) {
|
|
cache[cKey] = data[1];
|
|
// Build
|
|
callback( {
|
|
'query': query,
|
|
'results': data[1]
|
|
} );
|
|
}
|
|
} );
|
|
}
|
|
},
|
|
// Called when multiSuggest dropdown is updated.
|
|
'update': function() {
|
|
// Ellipsis
|
|
$( '.ve-ui-suggest-item' )
|
|
.autoEllipsis( {
|
|
'hasSpan': true,
|
|
'tooltip': true
|
|
} );
|
|
},
|
|
// Position the iframe overlay below the input.
|
|
'position': function () {
|
|
context.positionIframeOverlay( {
|
|
'overlay': $overlay,
|
|
'below': inspector.$locationInput
|
|
} );
|
|
}
|
|
};
|
|
// Setup Multi Suggest
|
|
this.$locationInput.multiSuggest( options );
|
|
};
|