From 26edd30cc6108a9a3b875d94bb72be30bd2bac03 Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Sat, 13 Jun 2015 16:48:26 +0100 Subject: [PATCH] [BREAKING CHANGE] Merge in VE's link input functionality to title widget Major changes: * Supports page images and descriptions * Uses prefix search Change-Id: Ib463e60cad9651eb338701279f370711ade6030b --- languages/i18n/en.json | 4 +- languages/i18n/qqq.json | 4 +- resources/Resources.php | 4 +- .../mw.widgets.TitleInputWidget.css | 53 +++- .../mw.widgets.TitleInputWidget.js | 233 +++++++++++++++--- .../mw.widgets.TitleOptionWidget.js | 81 ++++++ 6 files changed, 336 insertions(+), 43 deletions(-) create mode 100644 resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js diff --git a/languages/i18n/en.json b/languages/i18n/en.json index de4119ee8e..25ae0a25e4 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -3779,5 +3779,7 @@ "special-characters-group-khmer": "Khmer", "special-characters-title-endash": "en dash", "special-characters-title-emdash": "em dash", - "special-characters-title-minus": "minus sign" + "special-characters-title-minus": "minus sign", + "mw-widgets-titleinput-description-new-page": "page does not exist yet", + "mw-widgets-titleinput-description-redirect": "redirect to $1" } diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 96381380f1..7288066925 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -3949,5 +3949,7 @@ "special-characters-group-khmer": "{{Identical|Khmer}}", "special-characters-title-endash": "Title tooltip for the en dash character (–); See https://en.wikipedia.org/wiki/Dash", "special-characters-title-emdash": "Title tooltip for the em dash character (—); See https://en.wikipedia.org/wiki/Dash", - "special-characters-title-minus": "Title tooltip for the minus sign character (−), not to be confused with a hyphen" + "special-characters-title-minus": "Title tooltip for the minus sign character (−), not to be confused with a hyphen", + "mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.", + "mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget." } diff --git a/resources/Resources.php b/resources/Resources.php index d75c8a180c..f7a05318f4 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1735,6 +1735,7 @@ return array( 'scripts' => array( 'resources/src/mediawiki.widgets/mw.widgets.js', 'resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js', + 'resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js', ), 'skinStyles' => array( 'default' => 'resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css', @@ -1745,7 +1746,8 @@ return array( 'oojs-ui', ), 'messages' => array( - // … + 'mw-widgets-titleinput-description-new-page', + 'mw-widgets-titleinput-description-redirect', ), 'targets' => array( 'desktop', 'mobile' ), ), diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css index 0065f70ba9..2c24b2bbab 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css @@ -1,10 +1,57 @@ /*! - * MediaWiki Widgets – TitleInputWidget styles. + * MediaWiki Widgets - TitleInputWidget styles. * * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ -.mw-widget-TitleInputWidget { - width: 30em; +.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + min-height: 3.75em; + margin-left: 3.75em; +} + +.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget:not(:last-child) { + margin-bottom: 1px; +} + +.mw-widget-titleInputWidget-menu-withImages .oo-ui-iconElement .oo-ui-iconElement-icon { + display: block; + width: 3.75em; + height: 3.75em; + left: -3.75em; + background-color: #ccc; + opacity: 0.4; +} + +.mw-widget-titleInputWidget-menu-withImages .oo-ui-iconElement .mw-widget-titleOptionWidget-hasImage { + border: 0; + background-size: cover; + opacity: 1; +} + +.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget .oo-ui-labelElement-label { + line-height: 2.8em; +} + +.mw-widget-titleOptionWidget-description { + display: none; +} + +.mw-widget-titleInputWidget-menu-withDescriptions .mw-widget-titleOptionWidget .oo-ui-labelElement-label { + line-height: 1.5em; +} + +.mw-widget-titleInputWidget-menu-withDescriptions .mw-widget-titleOptionWidget-description { + display: block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.oo-ui-menuOptionWidget:not(.oo-ui-optionWidget-selected) .mw-widget-titleOptionWidget-description, +.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted .mw-widget-titleOptionWidget-description { + color: #888; } diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js index 26b2f5d792..221de0f0a2 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js @@ -1,5 +1,5 @@ /*! - * 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 @@ -14,9 +14,17 @@ * * @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 || {}; @@ -27,11 +35,34 @@ 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 */ @@ -55,60 +86,188 @@ /** * @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 * diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js new file mode 100644 index 0000000000..07b81e48db --- /dev/null +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js @@ -0,0 +1,81 @@ +/*! + * 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( '' ); + 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( + $( '' ) + .addClass( 'mw-widget-titleOptionWidget-description' ) + .text( config.description ) + ); + } + }; + + /* Inheritance */ + + OO.inheritClass( mw.widgets.TitleOptionWidget, OO.ui.MenuOptionWidget ); + +}( jQuery, mediaWiki ) ); -- 2.20.1