mediawiki-extensions-WikiEd.../modules/insertlink/TitleInputWidget.js
Sam Wilson d96435cd18 Prevent external links from showing as invalid
* When the link target text is changed, only change the URL mode if the user
  hasn't explicitely set the type (internal/external).
* Improve the conditions for the display of the 'external' and 'exists'
  link-target messages.
* Validate the link target (i.e. show messages) not only when the text is
  changed but also when the target type radio is changed.
* Rename the TitleInputWidget.isExternalLink() method to
  looksLikeExternalLink(), to make its purpose clearer.

Bug: T293168
Change-Id: Ie40e8bdebe6f1330fc75ea1861f120e51ad58224
2021-12-06 15:24:04 +08:00

157 lines
4.7 KiB
JavaScript

var InsertLinkTitleOptionWidget = require( './TitleOptionWidget.js' );
/**
* A custom TitleInputWidget that adds support for external links
* (any string that starts with a protocol or `www.` and doesn't exist as a page).
*
* @class
* @extends mw.widgets.TitleInputWidget
* @constructor
*/
function TitleInputWidget() {
TitleInputWidget.parent.call( this, {
showImages: true,
showDescriptions: true,
showDisambigsLast: true,
placeholder: mw.msg( 'wikieditor-toolbar-tool-link-int-target-tooltip' ),
$overlay: this.getOverlay(),
validateTitle: false,
showInterwikis: true,
required: true,
addQueryInput: true
} );
}
OO.inheritClass( TitleInputWidget, mw.widgets.TitleInputWidget );
/**
* Regular expression for determining what might be an external link.
*
* @static
* @property {RegExp}
*/
TitleInputWidget.static.urlRegex = new RegExp( '^(' + mw.config.get( 'wgUrlProtocols' ) + '|www\\.)', 'i' );
/**
* When leaving the input without selecting a menu item,
* automatically select a matching item if there is one.
* Even though we specify addQueryInput=true in the config, the entered string
* is not always available to be selected. See T291056.
*/
TitleInputWidget.prototype.onLookupInputBlur = function () {
TitleInputWidget.parent.prototype.onLookupInputBlur.apply( this );
this.selectFirstMatch();
};
/**
* Select the first matching search result
* The first match might not be at the top of the list, nor an exact match.
*
* @public
*/
TitleInputWidget.prototype.selectFirstMatch = function () {
var that = this;
this.getLookupMenuItems().done( function ( items ) {
// The matching item is not always the first,
// because disambiguation pages are moved to the end.
for ( var i = 0; i < items.length; i++ ) {
var item = items[ i ];
var queryVal = that.getQueryValue();
// Check for exact match, or a match with uppercase first character.
if ( item.getData() === queryVal ||
item.getData() === queryVal.charAt( 0 ).toUpperCase() + queryVal.slice( 1 )
) {
// If a matching title is is found, fire an event and stop looking.
that.emit( 'select', item );
break;
}
}
} );
};
/**
* Get menu option widget data from the title and page data,
* adding an `external` property.
*
* @param {string} title Page title
* @param {Object} data Page data
* @return {Object} Data for option widget
*/
TitleInputWidget.prototype.getOptionWidgetData = function ( title, data ) {
var widgetData = TitleInputWidget.parent.prototype.getOptionWidgetData.call( this, title, data );
widgetData.external = data.originalData.external;
return widgetData;
};
/**
* Create a InsertLinkTitleOptionWidget.
*
* @param {Object} data Data for option widget
* @return {OO.ui.MenuOptionWidget} The option widget
*/
TitleInputWidget.prototype.createOptionWidget = function ( data ) {
return new InsertLinkTitleOptionWidget( data );
};
/**
* Get pages' data from the API response, adding an `external` property for
* pages that do not exist and which look like external URLs.
*
* @param {Object} response
* @return {Object}
*/
TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
var res = TitleInputWidget.parent.prototype.getLookupCacheDataFromResponse( response );
// Guard against zero responses.
if ( res.pages === undefined ) {
return res;
}
for ( var pageId in res.pages ) {
if ( Object.prototype.hasOwnProperty.call( res.pages, pageId ) ) {
var page = res.pages[ pageId ];
page.external = page.missing !== undefined && this.looksLikeExternalLink( page.title );
}
}
return res;
};
/**
* Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
*
* @param {OO.ui.MenuOptionWidget} item Selected item
*/
TitleInputWidget.prototype.onLookupMenuChoose = function ( item ) {
TitleInputWidget.parent.prototype.onLookupMenuChoose.call( this, item );
this.emit( 'select', item );
};
/**
* Get a custom overlay for the dropdown menu, so it's not contained within the jQuery UI dialog.
*
* @private
* @return {jQuery}
*/
TitleInputWidget.prototype.getOverlay = function () {
// Overlay z-index must be greater than the jQuery UI dialog's of 1002.
var $overlay = OO.ui.getDefaultOverlay()
.clone()
.css( 'z-index', '1010' );
$( document.body ).append( $overlay );
return $overlay;
};
/**
* Determine if a given string is likely to be an external URL.
* External URLs start with either a valid protocol (from $wgUrlProtocols) or `www.`.
*
* @public
* @param {string} urlString The possible URL.
* @return {boolean}
*/
TitleInputWidget.prototype.looksLikeExternalLink = function ( urlString ) {
var matches = urlString.match( this.constructor.static.urlRegex );
return matches !== null && matches.length > 0;
};
module.exports = TitleInputWidget;