diff options
Diffstat (limited to 'www/wiki/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js')
-rw-r--r-- | www/wiki/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/www/wiki/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js b/www/wiki/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js new file mode 100644 index 00000000..2bbeabf5 --- /dev/null +++ b/www/wiki/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js @@ -0,0 +1,375 @@ +/*! + * MediaWiki Widgets - TitleWidget class. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +( function ( $, mw ) { + var hasOwn = Object.prototype.hasOwnProperty; + + /** + * Mixin for title widgets + * + * @class + * @abstract + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} [limit=10] Number of results to show + * @cfg {number} [namespace] Namespace to prepend to queries + * @cfg {number} [maxLength=255] Maximum query length + * @cfg {boolean} [relative=true] If a namespace is set, display titles relative to it + * @cfg {boolean} [suggestions=true] Display search suggestions + * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects + * @cfg {boolean} [showImages] Show page images + * @cfg {boolean} [showDescriptions] Show page descriptions + * @cfg {boolean} [showMissing=true] Show missing pages + * @cfg {boolean} [addQueryInput=true] Add exact user's input query to results + * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions + * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true, + * the widget will marks itself red for invalid inputs, including an empty query). + * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument + * @cfg {mw.Api} [api] API object to use, creates a default mw.Api instance if not specified + */ + mw.widgets.TitleWidget = function MwWidgetsTitleWidget( config ) { + // Config initialization + config = $.extend( { + maxLength: 255, + limit: 10 + }, config ); + + // Properties + this.limit = config.limit; + this.maxLength = config.maxLength; + this.namespace = config.namespace !== undefined ? config.namespace : null; + this.relative = config.relative !== undefined ? config.relative : true; + this.suggestions = config.suggestions !== undefined ? config.suggestions : true; + this.showRedirectTargets = config.showRedirectTargets !== false; + this.showImages = !!config.showImages; + this.showDescriptions = !!config.showDescriptions; + this.showMissing = config.showMissing !== false; + this.addQueryInput = config.addQueryInput !== false; + this.excludeCurrentPage = !!config.excludeCurrentPage; + this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true; + this.cache = config.cache; + this.api = config.api || new mw.Api(); + // Supports: IE10, FF28, Chrome23 + this.compare = window.Intl && Intl.Collator ? + new Intl.Collator( mw.config.get( 'wgContentLanguage' ), { sensitivity: 'base' } ).compare : + null; + + // Initialization + this.$element.addClass( 'mw-widget-titleWidget' ); + }; + + /* Setup */ + + OO.initClass( mw.widgets.TitleWidget ); + + /* Static properties */ + + mw.widgets.TitleWidget.static.interwikiPrefixesPromiseCache = {}; + + /* Methods */ + + /** + * Get the current value of the search query + * + * @abstract + * @return {string} Search query + */ + mw.widgets.TitleWidget.prototype.getQueryValue = null; + + /** + * Get the namespace to prepend to titles in suggestions, if any. + * + * @return {number|null} Namespace number + */ + mw.widgets.TitleWidget.prototype.getNamespace = function () { + return this.namespace; + }; + + /** + * Set the namespace to prepend to titles in suggestions, if any. + * + * @param {number|null} namespace Namespace number + */ + mw.widgets.TitleWidget.prototype.setNamespace = function ( namespace ) { + this.namespace = namespace; + }; + + mw.widgets.TitleWidget.prototype.getInterwikiPrefixesPromise = function () { + var api = this.getApi(), + cache = this.constructor.static.interwikiPrefixesPromiseCache, + key = api.defaults.ajax.url; + if ( !cache.hasOwnProperty( key ) ) { + cache[ key ] = api.get( { + action: 'query', + meta: 'siteinfo', + siprop: 'interwikimap', + // Cache client-side for a day since this info is mostly static + maxage: 60 * 60 * 24, + smaxage: 60 * 60 * 24, + // Workaround T97096 by setting uselang=content + uselang: 'content' + } ).then( function ( data ) { + return $.map( data.query.interwikimap, function ( interwiki ) { + return interwiki.prefix; + } ); + } ); + } + return cache[ key ]; + }; + + /** + * Get a promise which resolves with an API repsonse for suggested + * links for the current query. + * + * @return {jQuery.Promise} Suggestions promise + */ + mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () { + var req, + api = this.getApi(), + query = this.getQueryValue(), + widget = this, + promiseAbortObject = { abort: function () { + // Do nothing. This is just so OOUI doesn't break due to abort being undefined. + } }; + + if ( !mw.Title.newFromText( query ) ) { + // 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 ); + } + + return this.getInterwikiPrefixesPromise().then( function ( interwikiPrefixes ) { + var interwiki = query.substring( 0, query.indexOf( ':' ) ); + if ( + interwiki && interwiki !== '' && + interwikiPrefixes.indexOf( interwiki ) !== -1 + ) { + return $.Deferred().resolve( { query: { + pages: [ { + title: query + } ] + } } ).promise( promiseAbortObject ); + } else { + req = api.get( widget.getApiParams( query ) ); + promiseAbortObject.abort = req.abort.bind( req ); // TODO ew + return req.then( function ( ret ) { + if ( widget.showMissing && ret.query === undefined ) { + ret = api.get( { action: 'query', titles: query } ); + promiseAbortObject.abort = ret.abort.bind( ret ); + } + return ret; + } ); + } + } ).promise( promiseAbortObject ); + }; + + /** + * Get API params for a given query + * + * @param {string} query User query + * @return {Object} API params + */ + mw.widgets.TitleWidget.prototype.getApiParams = function ( query ) { + var params = { + action: 'query', + prop: [ 'info', 'pageprops' ], + generator: 'prefixsearch', + gpssearch: query, + gpsnamespace: this.namespace !== null ? this.namespace : undefined, + gpslimit: this.limit, + ppprop: 'disambiguation' + }; + if ( this.showRedirectTargets ) { + params.redirects = true; + } + if ( this.showImages ) { + params.prop.push( 'pageimages' ); + params.pithumbsize = 80; + params.pilimit = this.limit; + } + if ( this.showDescriptions ) { + params.prop.push( 'description' ); + } + return params; + }; + + /** + * Get the API object for title requests + * + * @return {mw.Api} MediaWiki API + */ + mw.widgets.TitleWidget.prototype.getApi = function () { + return this.api; + }; + + /** + * Get option widgets from the server response + * + * @param {Object} data Query result + * @return {OO.ui.OptionWidget[]} Menu items + */ + mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) { + var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects, + currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(), + items = [], + titles = [], + titleObj = mw.Title.newFromText( this.getQueryValue() ), + 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 ]; + + // When excludeCurrentPage is set, don't list the current page unless the user has type the full title + if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) { + continue; + } + pageData[ suggestionPage.title ] = { + known: suggestionPage.known !== undefined, + missing: suggestionPage.missing !== undefined, + redirect: suggestionPage.redirect !== undefined, + disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined, + imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ), + description: suggestionPage.description, + // Sort index + index: suggestionPage.index, + originalData: suggestionPage + }; + + // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true + // and we encounter a cross-namespace redirect. + if ( this.namespace === null || this.namespace === suggestionPage.ns ) { + titles.push( suggestionPage.title ); + } + + redirects = hasOwn.call( redirectsTo, suggestionPage.title ) ? redirectsTo[ suggestionPage.title ] : []; + for ( i = 0, len = redirects.length; i < len; i++ ) { + pageData[ redirects[ i ] ] = { + missing: false, + known: true, + redirect: true, + disambiguation: false, + description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title ), + // Sort index, just below its target + index: suggestionPage.index + 0.5, + originalData: suggestionPage + }; + titles.push( redirects[ i ] ); + } + } + + titles.sort( function ( a, b ) { + return pageData[ a ].index - pageData[ b ].index; + } ); + + // If not found, run value through mw.Title to avoid treating a match as a + // mismatch where normalisation would make them matching (T50476) + + pageExistsExact = ( + hasOwn.call( pageData, this.getQueryValue() ) && + ( + !pageData[ this.getQueryValue() ].missing || + pageData[ this.getQueryValue() ].known + ) + ); + pageExists = pageExistsExact || ( + titleObj && + hasOwn.call( pageData, titleObj.getPrefixedText() ) && + ( + !pageData[ titleObj.getPrefixedText() ].missing || + pageData[ titleObj.getPrefixedText() ].known + ) + ); + + if ( this.cache ) { + this.cache.set( pageData ); + } + + // Offer the exact text as a suggestion if the page exists + if ( this.addQueryInput && pageExists && !pageExistsExact ) { + titles.unshift( this.getQueryValue() ); + } + + for ( i = 0, len = titles.length; i < len; i++ ) { + page = hasOwn.call( pageData, titles[ i ] ) ? pageData[ titles[ i ] ] : {}; + items.push( this.createOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) ); + } + + return items; + }; + + /** + * Create a menu option widget with specified data + * + * @param {Object} data Data for option widget + * @return {OO.ui.MenuOptionWidget} Data for option widget + */ + mw.widgets.TitleWidget.prototype.createOptionWidget = function ( data ) { + return new mw.widgets.TitleOptionWidget( data ); + }; + + /** + * Get menu option widget data from the title and page data + * + * @param {string} title Title object + * @param {Object} data Page data + * @return {Object} Data for option widget + */ + mw.widgets.TitleWidget.prototype.getOptionWidgetData = function ( title, data ) { + var mwTitle = new mw.Title( title ), + description = data.description; + if ( data.missing && !description ) { + description = mw.msg( 'mw-widgets-titleinput-description-new-page' ); + } + return { + data: this.namespace !== null && this.relative ? + mwTitle.getRelativeText( this.namespace ) : + title, + url: mwTitle.getUrl(), + showImages: this.showImages, + imageUrl: this.showImages ? data.imageUrl : null, + description: this.showDescriptions ? description : null, + missing: data.missing, + redirect: data.redirect, + disambiguation: data.disambiguation, + query: this.getQueryValue(), + compare: this.compare + }; + }; + + /** + * Get title object corresponding to given value, or #getQueryValue if not given. + * + * @param {string} [value] Value to get a title for + * @return {mw.Title|null} Title object, or null if value is invalid + */ + mw.widgets.TitleWidget.prototype.getMWTitle = function ( value ) { + var title = value !== undefined ? value : this.getQueryValue(), + // mw.Title doesn't handle null well + titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined ); + + return titleObj; + }; + + /** + * Check if the query is valid + * + * @return {boolean} The query is valid + */ + mw.widgets.TitleWidget.prototype.isQueryValid = function () { + return this.validateTitle ? !!this.getMWTitle() : true; + }; + +}( jQuery, mediaWiki ) ); |