mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 18:39:52 +00:00
f06952f2f3
Refactor: * ve.indexOf Renamed from ve.inArray. This was named after the jQuery method which in turn has a longer story about why it is so unfortunately named. It doesn't return a boolean, but an index. Hence the native method being called indexOf as well. * ve.bind Renamed from ve.proxy. I considered making it use Function.prototype.bind if available. As it performs better than $.proxy (which doesn't use to the native bind if available). However since bind needs to be bound itself in order to use it detached, it turns out with the "call()" and "bind()" it is slower than the $.proxy shim: http://jsperf.com/function-bind-shim-perf It would've been like this: ve.bind = Function.prototype.bind ? Function.prototype.call.bind( Function.prototype.bind ) : $.proxy; But instead sticking to ve.bind = $.proxy; * ve.extendObject Documented the parts of jQuery.extend that we use. This makes it easier to replace in the future. Documentation: * Added function documentation blocks. * Added annotations to functions that we will be able to remove in the future in favour of the native methods. With "@until + when/how". In this case "ES5". Meaning, whenever we drop support for browsers that don't support ES5. Although in the developer community ES5 is still fairly fresh, browsers have been aware for it long enough that thee moment we're able to drop it may be sooner than we think. The only blocker so far is IE8. The rest of the browsers have had it long enough that the traffic we need to support of non-IE supports it. Misc.: * Removed 'node: true' from .jshintrc since Parsoid is no longer in this repo and thus no more nodejs files. - This unraveled two lint errors: Usage of 'module' and 'console'. (both were considered 'safe globals' due to nodejs, but not in browser code). * Replaced usage (before renaming): - $.inArray -> ve.inArray - Function.prototype.bind -> ve.proxy - Array.isArray -> ve.isArray - [].indexOf -> ve.inArray - $.fn.bind/live/delegate/unbind/die/delegate -> $.fn.on/off Change-Id: Idcf1fa6a685b6ed3d7c99ffe17bd57a7bc586a2c
234 lines
7 KiB
JavaScript
234 lines
7 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
|
|
* @param {ve.ui.Toolbar} toolbar
|
|
*/
|
|
ve.ui.LinkInspector = function ( toolbar, context ) {
|
|
// Inheritance
|
|
ve.ui.Inspector.call( this, toolbar, context );
|
|
|
|
// Properties
|
|
this.$clearButton = $( '<div class="es-inspector-button es-inspector-clearButton"></div>', context.inspectorDoc )
|
|
.prependTo( this.$ );
|
|
this.$.prepend(
|
|
$( '<div class="es-inspector-title"></div>', context.inspectorDoc )
|
|
.text( ve.msg( 'visualeditor-linkinspector-title' ) )
|
|
);
|
|
this.$locationLabel = $( '<label>', context.inspectorDoc )
|
|
.text( ve.msg( 'visualeditor-linkinspector-label-pagetitle' ) )
|
|
.appendTo( this.$form );
|
|
this.$locationInput = $( '<input type="text">', context.inspectorDoc ).appendTo( this.$form );
|
|
this.initialValue = null;
|
|
|
|
// Events
|
|
var inspector = this;
|
|
this.$clearButton.click( function () {
|
|
if ( $(this).is( '.es-inspector-button-disabled' ) ) {
|
|
return;
|
|
}
|
|
|
|
var hash,
|
|
surfaceModel = inspector.context.getSurfaceView().getModel(),
|
|
annotations = inspector.getAllLinkAnnotationsFromSelection();
|
|
// Clear all link annotations.
|
|
for ( hash in annotations ) {
|
|
surfaceModel.annotate( 'clear', annotations[hash] );
|
|
}
|
|
inspector.$locationInput.val( '' );
|
|
inspector.context.closeInspector();
|
|
} );
|
|
this.$locationInput.on( 'mousedown keydown cut paste', function () {
|
|
setTimeout( function () {
|
|
if ( inspector.$locationInput.val() !== '' ) {
|
|
inspector.$acceptButton.removeClass( 'es-inspector-button-disabled' );
|
|
} else {
|
|
inspector.$acceptButton.addClass( 'es-inspector-button-disabled' );
|
|
}
|
|
}, 0 );
|
|
} );
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
ve.ui.LinkInspector.prototype.getAllLinkAnnotationsFromSelection = function () {
|
|
var surfaceView = this.context.getSurfaceView(),
|
|
surfaceModel = surfaceView.getModel(),
|
|
documentModel = surfaceModel.getDocument(),
|
|
annotations,
|
|
linkAnnotations = {};
|
|
|
|
annotations = documentModel.getAnnotationsFromRange( surfaceModel.getSelection(), true );
|
|
// XXX: '.' is not escaped, is the '.*' part redundant?
|
|
linkAnnotations = ve.dm.Document.getMatchingAnnotations ( annotations, /^link\// );
|
|
if ( !ve.isEmptyObject( linkAnnotations ) ) {
|
|
return linkAnnotations;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
ve.ui.LinkInspector.prototype.getFirstLinkAnnotation = function ( annotations ) {
|
|
var hash;
|
|
for ( hash in annotations ) {
|
|
// Use the first one with a recognized type (there should only be one, this is just in case)
|
|
if (
|
|
annotations[hash].type === 'link/WikiLink' ||
|
|
annotations[hash].type === 'link/ExtLink'
|
|
) {
|
|
return annotations[hash];
|
|
}
|
|
}
|
|
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( 'es-inspector-button-disabled' );
|
|
} else if ( annotation.type === 'link/WikiLink' ) {
|
|
// Internal link
|
|
initialValue = annotation.data.title || '';
|
|
this.$locationInput.val( initialValue );
|
|
this.$clearButton.removeClass( 'es-inspector-button-disabled' );
|
|
} else {
|
|
// External link
|
|
initialValue = annotation.data.href || '';
|
|
this.$locationInput.val( initialValue );
|
|
this.$clearButton.removeClass( 'es-inspector-button-disabled' );
|
|
}
|
|
|
|
this.initialValue = initialValue;
|
|
if ( this.$locationInput.val().length === 0 ) {
|
|
this.$acceptButton.addClass( 'es-inspector-button-disabled' );
|
|
} else {
|
|
this.$acceptButton.removeClass( 'es-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(),
|
|
hash, annotation;
|
|
if ( accept ) {
|
|
if ( !target ) {
|
|
return;
|
|
}
|
|
// Clear link annotation if it exists
|
|
for ( hash in annotations ) {
|
|
surfaceModel.annotate( 'clear', annotations[hash] );
|
|
}
|
|
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&validity
|
|
// checks using AJAX
|
|
try {
|
|
// FIXME mw dependency
|
|
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 }
|
|
};
|
|
}
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
ve.extendClass( ve.ui.LinkInspector, ve.ui.Inspector );
|