Merge "user: Allow "CAS update failed" exceptions to be normalised"
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets / mw.widgets.CategoryTagItemWidget.js
1 /*!
2 * MediaWiki Widgets - CategoryTagItemWidget class.
3 *
4 * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
6 */
7 ( function () {
8
9 var hasOwn = Object.prototype.hasOwnProperty;
10
11 /**
12 * @class mw.widgets.PageExistenceCache
13 * @private
14 * @param {mw.Api} [api]
15 */
16 function PageExistenceCache( api ) {
17 this.api = api || new mw.Api();
18 this.processExistenceCheckQueueDebounced = OO.ui.debounce( this.processExistenceCheckQueue );
19 this.currentRequest = null;
20 this.existenceCache = {};
21 this.existenceCheckQueue = {};
22 }
23
24 /**
25 * Check for existence of pages in the queue.
26 *
27 * @private
28 */
29 PageExistenceCache.prototype.processExistenceCheckQueue = function () {
30 var queue, titles,
31 cache = this;
32 if ( this.currentRequest ) {
33 // Don't fire off a million requests at the same time
34 this.currentRequest.always( function () {
35 cache.currentRequest = null;
36 cache.processExistenceCheckQueueDebounced();
37 } );
38 return;
39 }
40 queue = this.existenceCheckQueue;
41 this.existenceCheckQueue = {};
42 titles = Object.keys( queue ).filter( function ( title ) {
43 if ( hasOwn.call( cache.existenceCache, title ) ) {
44 queue[ title ].resolve( cache.existenceCache[ title ] );
45 }
46 return !hasOwn.call( cache.existenceCache, title );
47 } );
48 if ( !titles.length ) {
49 return;
50 }
51 this.currentRequest = this.api.get( {
52 formatversion: 2,
53 action: 'query',
54 prop: [ 'info' ],
55 titles: titles
56 } ).done( function ( response ) {
57 var
58 normalized = {},
59 pages = {};
60 ( response.query.normalized || [] ).forEach( function ( data ) {
61 normalized[ data.fromencoded ? decodeURIComponent( data.from ) : data.from ] = data.to;
62 } );
63 response.query.pages.forEach( function ( page ) {
64 pages[ page.title ] = !page.missing;
65 } );
66 titles.forEach( function ( title ) {
67 var normalizedTitle = title;
68 while ( hasOwn.call( normalized, normalizedTitle ) ) {
69 normalizedTitle = normalized[ normalizedTitle ];
70 }
71 cache.existenceCache[ title ] = pages[ normalizedTitle ];
72 queue[ title ].resolve( cache.existenceCache[ title ] );
73 } );
74 } );
75 };
76
77 /**
78 * Register a request to check whether a page exists.
79 *
80 * @private
81 * @param {mw.Title} title
82 * @return {jQuery.Promise} Promise resolved with true if the page exists or false otherwise
83 */
84 PageExistenceCache.prototype.checkPageExistence = function ( title ) {
85 var key = title.getPrefixedText();
86 if ( !hasOwn.call( this.existenceCheckQueue, key ) ) {
87 this.existenceCheckQueue[ key ] = $.Deferred();
88 }
89 this.processExistenceCheckQueueDebounced();
90 return this.existenceCheckQueue[ key ].promise();
91 };
92
93 /**
94 * @class mw.widgets.ForeignTitle
95 * @private
96 * @extends mw.Title
97 *
98 * @constructor
99 * @param {string} title
100 * @param {number} [namespace]
101 */
102 function ForeignTitle( title, namespace ) {
103 // We only need to handle categories here... but we don't know the target language.
104 // So assume that any namespace-like prefix is the 'Category' namespace...
105 title = title.replace( /^(.+?)_*:_*(.*)$/, 'Category:$2' ); // HACK
106 ForeignTitle.parent.call( this, title, namespace );
107 }
108 OO.inheritClass( ForeignTitle, mw.Title );
109 ForeignTitle.prototype.getNamespacePrefix = function () {
110 // We only need to handle categories here...
111 return 'Category:'; // HACK
112 };
113
114 /**
115 * Category selector capsule item widget. Extends OO.ui.CapsuleItemWidget with the ability to link
116 * to the given page, and to show its existence status (i.e., whether it is a redlink).
117 *
118 * @class mw.widgets.CategoryTagItemWidget
119 * @uses mw.Api
120 * @extends OO.ui.TagItemWidget
121 *
122 * @constructor
123 * @param {Object} config Configuration options
124 * @cfg {mw.Title} title Page title to use (required)
125 * @cfg {string} [apiUrl] API URL, if not the current wiki's API
126 */
127 mw.widgets.CategoryTagItemWidget = function MWWCategoryTagItemWidget( config ) {
128 var widget = this;
129 // Parent constructor
130 mw.widgets.CategoryTagItemWidget.parent.call( this, $.extend( {
131 data: config.title.getMainText(),
132 label: config.title.getMainText()
133 }, config ) );
134
135 // Properties
136 this.title = config.title;
137 this.apiUrl = config.apiUrl || '';
138 this.$link = $( '<a>' )
139 .text( this.label )
140 .attr( 'target', '_blank' )
141 .on( 'click', function ( e ) {
142 // TagMultiselectWidget really wants to prevent you from clicking the link, don't let it
143 e.stopPropagation();
144 } );
145
146 // Initialize
147 this.setMissing( false );
148 this.$label.replaceWith( this.$link );
149 this.setLabelElement( this.$link );
150
151 if ( !this.constructor.static.pageExistenceCaches[ this.apiUrl ] ) {
152 this.constructor.static.pageExistenceCaches[ this.apiUrl ] =
153 new PageExistenceCache( new mw.ForeignApi( this.apiUrl ) );
154 }
155 this.constructor.static.pageExistenceCaches[ this.apiUrl ]
156 .checkPageExistence( new ForeignTitle( this.title.getPrefixedText() ) )
157 .done( function ( exists ) {
158 widget.setMissing( !exists );
159 } );
160 };
161
162 /* Setup */
163
164 OO.inheritClass( mw.widgets.CategoryTagItemWidget, OO.ui.TagItemWidget );
165
166 /* Static Properties */
167
168 /**
169 * Map of API URLs to PageExistenceCache objects.
170 *
171 * @static
172 * @inheritable
173 * @property {Object}
174 */
175 mw.widgets.CategoryTagItemWidget.static.pageExistenceCaches = {
176 '': new PageExistenceCache()
177 };
178
179 /* Methods */
180
181 /**
182 * Update label link href and CSS classes to reflect page existence status.
183 *
184 * @private
185 * @param {boolean} missing Whether the page is missing (does not exist)
186 */
187 mw.widgets.CategoryTagItemWidget.prototype.setMissing = function ( missing ) {
188 var
189 title = new ForeignTitle( this.title.getPrefixedText() ), // HACK
190 prefix = this.apiUrl.replace( '/w/api.php', '' ); // HACK
191
192 this.missing = missing;
193
194 if ( !missing ) {
195 this.$link
196 .attr( 'href', prefix + title.getUrl() )
197 .attr( 'title', title.getPrefixedText() )
198 .removeClass( 'new' );
199 } else {
200 this.$link
201 .attr( 'href', prefix + title.getUrl( { action: 'edit', redlink: 1 } ) )
202 .attr( 'title', mw.msg( 'red-link-title', title.getPrefixedText() ) )
203 .addClass( 'new' );
204 }
205 };
206
207 // For backwards compatibility. See T183299.
208 mw.widgets.CategoryCapsuleItemWidget = mw.widgets.CategoryTagItemWidget;
209 }() );