/*!
- * MediaWiki Widgets – TitleInputWidget class.
+ * MediaWiki Widgets - TitleInputWidget class.
*
* @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {number} [limit=10] Number of results to show
* @cfg {number} [namespace] Namespace to prepend to queries
+ * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
+ * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
+ * @cfg {boolean} [showImages] Show page images
+ * @cfg {boolean} [showDescriptions] Show page descriptions
+ * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
*/
- mw.widgets.TitleInputWidget = function MWWTitleInputWidget( config ) {
+ mw.widgets.TitleInputWidget = function MwWidgetsTitleInputWidget( config ) {
+ var widget = this;
+
// Config initialization
config = config || {};
OO.ui.mixin.LookupElement.call( this, config );
// Properties
+ this.limit = config.limit || 10;
this.namespace = config.namespace || null;
+ this.showRedirectTargets = config.showRedirectTargets !== false;
+ this.showRedlink = !!config.showRedlink;
+ this.showImages = !!config.showImages;
+ this.showDescriptions = !!config.showDescriptions;
+ this.cache = config.cache;
// Initialization
- this.$element.addClass( 'mw-widget-TitleInputWidget' );
- this.lookupMenu.$element.addClass( 'mw-widget-TitleInputWidget-menu' );
+ this.$element.addClass( 'mw-widget-titleInputWidget' );
+ this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu' );
+ if ( this.showImages ) {
+ this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withImages' );
+ }
+ if ( this.showDescriptions ) {
+ this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withDescriptions' );
+ }
+
+ this.interwikiPrefixes = [];
+ this.interwikiPrefixesPromise = new mw.Api().get( {
+ action: 'query',
+ meta: 'siteinfo',
+ siprop: 'interwikimap'
+ } ).done( function ( data ) {
+ $.each( data.query.interwikimap, function ( index, interwiki ) {
+ widget.interwikiPrefixes.push( interwiki.prefix );
+ } );
+ } );
};
/* Inheritance */
/**
* @inheritdoc
*/
- mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () {
- var value = this.value;
+ mw.widgets.TitleInputWidget.prototype.focus = function () {
+ var retval;
- // Prefix with default namespace name
- if ( this.namespace !== null && mw.Title.newFromText( value, this.namespace ) ) {
- value = mw.Title.newFromText( value, this.namespace ).getPrefixedText();
- }
+ // Prevent programmatic focus from opening the menu
+ this.setLookupsDisabled( true );
- // Dont send leading ':' to open search
- if ( value[0] === ':' ) {
- value = value.slice( 1 );
- }
+ // Parent method
+ retval = OO.ui.TextInputWidget.prototype.focus.apply( this, arguments );
- return new mw.Api().get( {
- action: 'opensearch',
- search: value,
- suggest: ''
- } );
+ this.setLookupsDisabled( false );
+
+ return retval;
};
/**
* @inheritdoc
*/
+ mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () {
+ var req,
+ widget = this,
+ promiseAbortObject = { abort: function () {
+ // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
+ } };
+
+ if ( mw.Title.newFromText( this.value ) ) {
+ return this.interwikiPrefixesPromise.then( function () {
+ var params, props,
+ interwiki = widget.value.substring( 0, widget.value.indexOf( ':' ) );
+ if (
+ interwiki && interwiki !== '' &&
+ widget.interwikiPrefixes.indexOf( interwiki ) !== -1
+ ) {
+ return $.Deferred().resolve( { query: {
+ pages: [{
+ title: widget.value
+ }]
+ } } ).promise( promiseAbortObject );
+ } else {
+ params = {
+ action: 'query',
+ generator: 'prefixsearch',
+ gpssearch: widget.value,
+ gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
+ gpslimit: widget.limit,
+ ppprop: 'disambiguation'
+ };
+ props = [ 'info', 'pageprops' ];
+ if ( widget.showRedirectTargets ) {
+ params.redirects = '1';
+ }
+ if ( widget.showImages ) {
+ props.push( 'pageimages' );
+ params.pithumbsize = 80;
+ params.pilimit = widget.limit;
+ }
+ if ( widget.showDescriptions ) {
+ props.push( 'pageterms' );
+ params.wbptterms = 'description';
+ }
+ params.prop = props.join( '|' );
+ req = new mw.Api().get( params );
+ promiseAbortObject.abort = req.abort.bind( req ); // todo: ew
+ return req;
+ }
+ } ).promise( promiseAbortObject );
+ } else {
+ // Don't send invalid titles to the API.
+ // Just pretend it returned nothing so we can show the 'invalid title' section
+ return $.Deferred().resolve( {} ).promise( promiseAbortObject );
+ }
+ };
+
+ /**
+ * Get lookup cache item from server response data.
+ *
+ * @method
+ * @param {Mixed} data Response from server
+ */
mw.widgets.TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( data ) {
- return data[1] || [];
+ return data.query || {};
};
/**
- * @inheritdoc
+ * Get list of menu items from a server response.
+ *
+ * @param {Object} data Query result
+ * @returns {OO.ui.MenuOptionWidget[]} Menu items
*/
mw.widgets.TitleInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
- var i, len, title, value,
+ var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
items = [],
- matchingPages = data;
-
- // Matching pages
- if ( matchingPages && matchingPages.length ) {
- for ( i = 0, len = matchingPages.length; i < len; i++ ) {
- title = new mw.Title( matchingPages[i] );
- if ( this.namespace !== null ) {
- value = title.getRelativeText( this.namespace );
- } else {
- value = title.getPrefixedText();
- }
- items.push( new OO.ui.MenuOptionWidget( {
- data: value,
- label: value
- } ) );
+ titles = [],
+ titleObj = mw.Title.newFromText( this.value ),
+ redirectsTo = {},
+ pageData = {};
+
+ if ( data.redirects ) {
+ for ( i = 0, len = data.redirects.length; i < len; i++ ) {
+ redirect = data.redirects[i];
+ redirectsTo[redirect.to] = redirectsTo[redirect.to] || [];
+ redirectsTo[redirect.to].push( redirect.from );
+ }
+ }
+
+ for ( index in data.pages ) {
+ suggestionPage = data.pages[index];
+ pageData[suggestionPage.title] = {
+ missing: suggestionPage.missing !== undefined,
+ redirect: suggestionPage.redirect !== undefined,
+ disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
+ imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
+ description: OO.getProp( suggestionPage, 'terms', 'description' )
+ };
+ titles.push( suggestionPage.title );
+
+ redirects = redirectsTo[suggestionPage.title] || [];
+ for ( i = 0, len = redirects.length; i < len; i++ ) {
+ pageData[redirects[i]] = {
+ missing: false,
+ redirect: true,
+ disambiguation: false,
+ description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title )
+ };
+ titles.push( redirects[i] );
}
}
+ // If not found, run value through mw.Title to avoid treating a match as a
+ // mismatch where normalisation would make them matching (bug 48476)
+
+ pageExistsExact = titles.indexOf( this.value ) !== -1;
+ pageExists = pageExistsExact || (
+ titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
+ );
+
+ if ( !pageExists ) {
+ pageData[this.value] = {
+ missing: true, redirect: false, disambiguation: false,
+ description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
+ };
+ }
+
+ if ( this.cache ) {
+ this.cache.set( pageData );
+ }
+
+ // Offer the exact text as a suggestion if the page exists
+ if ( pageExists && !pageExistsExact ) {
+ titles.unshift( this.value );
+ }
+ // Offer the exact text as a new page if the title is valid
+ if ( this.showRedlink && !pageExists && titleObj ) {
+ titles.push( this.value );
+ }
+ for ( i = 0, len = titles.length; i < len; i++ ) {
+ page = pageData[titles[i]] || {};
+ items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[i], page ) ) );
+ }
+
return items;
};
+ /**
+ * Get menu option widget data from the title and page data
+ *
+ * @param {mw.Title} title Title object
+ * @param {Object} data Page data
+ * @return {Object} Data for option widget
+ */
+ mw.widgets.TitleInputWidget.prototype.getOptionWidgetData = function ( title, data ) {
+ var mwTitle = new mw.Title( title );
+ return {
+ data: this.namespace !== null ? mwTitle.getRelativeText( this.namespace ) : title,
+ imageUrl: this.showImages ? data.imageUrl : null,
+ description: this.showDescriptions ? data.description : null,
+ missing: data.missing,
+ redirect: data.redirect,
+ disambiguation: data.disambiguation,
+ query: this.value
+ };
+ };
+
/**
* Get title object corresponding to #getValue
*
--- /dev/null
+/*!
+ * MediaWiki Widgets - TitleOptionWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * Creates a mw.widgets.TitleOptionWidget object.
+ *
+ * @class
+ * @extends OO.ui.MenuOptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [data] Page title
+ * @cfg {string} [imageUrl] Thumbnail image URL with URL encoding
+ * @cfg {string} [description] Page description
+ * @cfg {boolean} [missing] Page doesn't exist
+ * @cfg {boolean} [redirect] Page is a redirect
+ * @cfg {boolean} [disambiguation] Page is a disambiguation page
+ * @cfg {string} [query] Matching query string
+ */
+ mw.widgets.TitleOptionWidget = function MwWidgetsTitleOptionWidget( config ) {
+ var icon, title = config.data;
+
+ if ( config.missing ) {
+ icon = 'page-not-found';
+ } else if ( config.redirect ) {
+ icon = 'page-redirect';
+ } else if ( config.disambiguation ) {
+ icon = 'page-disambiguation';
+ } else {
+ icon = 'page-existing';
+ }
+
+ // Config initialization
+ config = $.extend( {
+ icon: icon,
+ label: title,
+ href: mw.util.getUrl( title ),
+ autoFitLabel: false
+ }, config );
+
+ // Parent constructor
+ OO.ui.MenuOptionWidget.call( this, config );
+
+ // Intialization
+ this.$label.wrap( '<a>' );
+ this.$link = this.$label.parent();
+ this.$link.attr( 'href', config.href );
+ this.$element.addClass( 'mw-widget-titleOptionWidget' );
+
+ // Highlight matching parts of link suggestion
+ this.$label.autoEllipsis( { hasSpan: false, tooltip: true, matchText: config.query } );
+
+ if ( config.missing ) {
+ this.$link.addClass( 'new' );
+ }
+
+ if ( config.imageUrl ) {
+ this.$icon
+ .addClass( 'mw-widget-titleOptionWidget-hasImage' )
+ .css( 'background-image', 'url(' + config.imageUrl + ')' );
+ }
+
+ if ( config.description ) {
+ this.$element.append(
+ $( '<span>' )
+ .addClass( 'mw-widget-titleOptionWidget-description' )
+ .text( config.description )
+ );
+ }
+ };
+
+ /* Inheritance */
+
+ OO.inheritClass( mw.widgets.TitleOptionWidget, OO.ui.MenuOptionWidget );
+
+}( jQuery, mediaWiki ) );