Merge "Add ParserFetchTemplate hook"
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.feedback.js
1 /*!
2 * mediawiki.feedback
3 *
4 * @author Ryan Kaldari, 2010
5 * @author Neil Kandalgaonkar, 2010-11
6 * @author Moriel Schottlender, 2015
7 * @since 1.19
8 */
9 /*global OO*/
10 ( function ( mw, $ ) {
11 /**
12 * This is a way of getting simple feedback from users. It's useful
13 * for testing new features -- users can give you feedback without
14 * the difficulty of opening a whole new talk page. For this reason,
15 * it also tends to collect a wider range of both positive and negative
16 * comments. However you do need to tend to the feedback page. It will
17 * get long relatively quickly, and you often get multiple messages
18 * reporting the same issue.
19 *
20 * It takes the form of thing on your page which, when clicked, opens a small
21 * dialog box. Submitting that dialog box appends its contents to a
22 * wiki page that you specify, as a new section.
23 *
24 * This feature works with any content model that defines a
25 * `mw.messagePoster.MessagePoster`.
26 *
27 * Minimal usage example:
28 *
29 * var feedback = new mw.Feedback();
30 * $( '#myButton' ).click( function () { feedback.launch(); } );
31 *
32 * You can also launch the feedback form with a prefilled subject and body.
33 * See the docs for the #launch() method.
34 *
35 * @class
36 * @constructor
37 * @param {Object} [config] Configuration object
38 * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
39 * feedback.
40 * @cfg {string} [apiUrl] api.php URL if the feedback page is on another wiki
41 * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
42 * title of the dialog box
43 * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/edit/form/1/"] URL where
44 * bugs can be posted
45 * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL
46 * where bugs can be listed
47 * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form.
48 * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory.
49 * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox.
50 * defaults to the message 'feedback-terms'.
51 */
52 mw.Feedback = function MwFeedback( config ) {
53 config = config || {};
54
55 this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
56
57 // Feedback page title
58 this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
59
60 this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle, config.apiUrl );
61
62 // Links
63 this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/edit/form/1/';
64 this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
65
66 // Terms of use
67 this.useragentCheckboxShow = !!config.showUseragentCheckbox;
68 this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory;
69 this.useragentCheckboxMessage = config.useragentCheckboxMessage ||
70 $( '<p>' ).append( mw.msg( 'feedback-terms' ) );
71
72 // Message dialog
73 this.thankYouDialog = new OO.ui.MessageDialog();
74 };
75
76 /* Initialize */
77 OO.initClass( mw.Feedback );
78
79 /* Static Properties */
80 mw.Feedback.static.windowManager = null;
81 mw.Feedback.static.dialog = null;
82
83 /* Methods */
84
85 /**
86 * Respond to dialog submit event. If the information was
87 * submitted, either successfully or with an error, open
88 * a MessageDialog to thank the user.
89 *
90 * @param {string} [status] A status of the end of operation
91 * of the main feedback dialog. Empty if the dialog was
92 * dismissed with no action or the user followed the button
93 * to the external task reporting site.
94 */
95 mw.Feedback.prototype.onDialogSubmit = function ( status ) {
96 var dialogConfig = {};
97 switch ( status ) {
98 case 'submitted':
99 dialogConfig = {
100 title: mw.msg( 'feedback-thanks-title' ),
101 message: $( '<span>' ).msg(
102 'feedback-thanks',
103 this.feedbackPageTitle.getNameText(),
104 $( '<a>' ).attr( {
105 target: '_blank',
106 href: this.feedbackPageTitle.getUrl()
107 } )
108 ),
109 actions: [
110 {
111 action: 'accept',
112 label: mw.msg( 'feedback-close' ),
113 flags: 'primary'
114 }
115 ]
116 };
117 break;
118 }
119
120 // Show the message dialog
121 if ( !$.isEmptyObject( dialogConfig ) ) {
122 this.constructor.static.windowManager.openWindow(
123 this.thankYouDialog,
124 dialogConfig
125 );
126 }
127 };
128
129 /**
130 * Modify the display form, and then open it, focusing interface on the subject.
131 *
132 * @param {Object} [contents] Prefilled contents for the feedback form.
133 * @param {string} [contents.subject] The subject of the feedback, as plaintext
134 * @param {string} [contents.message] The content of the feedback, as wikitext
135 */
136 mw.Feedback.prototype.launch = function ( contents ) {
137 // Dialog
138 if ( !this.constructor.static.dialog ) {
139 this.constructor.static.dialog = new mw.Feedback.Dialog();
140 this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
141 }
142 if ( !this.constructor.static.windowManager ) {
143 this.constructor.static.windowManager = new OO.ui.WindowManager();
144 this.constructor.static.windowManager.addWindows( [
145 this.constructor.static.dialog,
146 this.thankYouDialog
147 ] );
148 $( 'body' )
149 .append( this.constructor.static.windowManager.$element );
150 }
151 // Open the dialog
152 this.constructor.static.windowManager.openWindow(
153 this.constructor.static.dialog,
154 {
155 title: mw.msg( this.dialogTitleMessageKey ),
156 settings: {
157 messagePosterPromise: this.messagePosterPromise,
158 title: this.feedbackPageTitle,
159 dialogTitleMessageKey: this.dialogTitleMessageKey,
160 bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
161 bugsTaskListLink: this.bugsTaskListLink,
162 useragentCheckbox: {
163 show: this.useragentCheckboxShow,
164 mandatory: this.useragentCheckboxMandatory,
165 message: this.useragentCheckboxMessage
166 }
167 },
168 contents: contents
169 }
170 );
171 };
172
173 /**
174 * mw.Feedback Dialog
175 *
176 * @class
177 * @extends OO.ui.ProcessDialog
178 *
179 * @constructor
180 * @param {Object} config Configuration object
181 */
182 mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
183 // Parent constructor
184 mw.Feedback.Dialog.parent.call( this, config );
185
186 this.status = '';
187 this.feedbackPageTitle = null;
188 // Initialize
189 this.$element.addClass( 'mwFeedback-Dialog' );
190 };
191
192 OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
193
194 /* Static properties */
195 mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
196 mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
197 mw.Feedback.Dialog.static.size = 'medium';
198 mw.Feedback.Dialog.static.actions = [
199 {
200 action: 'submit',
201 label: mw.msg( 'feedback-submit' ),
202 flags: [ 'primary', 'progressive' ]
203 },
204 {
205 action: 'external',
206 label: mw.msg( 'feedback-external-bug-report-button' ),
207 flags: 'progressive'
208 },
209 {
210 action: 'cancel',
211 label: mw.msg( 'feedback-cancel' ),
212 flags: 'safe'
213 }
214 ];
215
216 /**
217 * @inheritdoc
218 */
219 mw.Feedback.Dialog.prototype.initialize = function () {
220 var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
221 feedbackFieldsetLayout, termsOfUseLabel;
222
223 // Parent method
224 mw.Feedback.Dialog.parent.prototype.initialize.call( this );
225
226 this.feedbackPanel = new OO.ui.PanelLayout( {
227 scrollable: false,
228 expanded: false,
229 padded: true
230 } );
231
232 this.$spinner = $( '<div>' )
233 .addClass( 'feedback-spinner' );
234
235 // Feedback form
236 this.feedbackMessageLabel = new OO.ui.LabelWidget( {
237 classes: [ 'mw-feedbackDialog-welcome-message' ]
238 } );
239 this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
240 indicator: 'required',
241 multiline: false
242 } );
243 this.feedbackMessageInput = new OO.ui.TextInputWidget( {
244 autosize: true,
245 multiline: true
246 } );
247 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
248 label: mw.msg( 'feedback-subject' )
249 } );
250 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
251 label: mw.msg( 'feedback-message' )
252 } );
253 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
254 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
255 classes: [ 'mw-feedbackDialog-feedback-form' ]
256 } );
257
258 // Useragent terms of use
259 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
260 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
261 classes: [ 'mw-feedbackDialog-feedback-terms' ],
262 align: 'inline'
263 } );
264
265 termsOfUseLabel = new OO.ui.LabelWidget( {
266 classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
267 label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
268 } );
269
270 this.feedbackPanel.$element.append(
271 this.feedbackMessageLabel.$element,
272 feedbackFieldsetLayout.$element,
273 this.useragentFieldLayout.$element,
274 termsOfUseLabel.$element
275 );
276
277 // Events
278 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
279 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
280 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
281 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
282
283 this.$body.append( this.feedbackPanel.$element );
284 };
285
286 /**
287 * Validate the feedback form
288 */
289 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
290 var isValid = (
291 (
292 !this.useragentMandatory ||
293 this.useragentCheckbox.isSelected()
294 ) &&
295 this.feedbackSubjectInput.getValue()
296 );
297
298 this.actions.setAbilities( { submit: isValid } );
299 };
300
301 /**
302 * @inheritdoc
303 */
304 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
305 return this.feedbackPanel.$element.outerHeight( true );
306 };
307
308 /**
309 * @inheritdoc
310 */
311 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
312 return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
313 .next( function () {
314 var plainMsg, parsedMsg,
315 settings = data.settings;
316 data.contents = data.contents || {};
317
318 // Prefill subject/message
319 this.feedbackSubjectInput.setValue( data.contents.subject );
320 this.feedbackMessageInput.setValue( data.contents.message );
321
322 this.status = '';
323 this.messagePosterPromise = settings.messagePosterPromise;
324 this.setBugReportLink( settings.bugsTaskSubmissionLink );
325 this.feedbackPageTitle = settings.title;
326 this.feedbackPageName = settings.title.getNameText();
327 this.feedbackPageUrl = settings.title.getUrl();
328
329 // Useragent checkbox
330 if ( settings.useragentCheckbox.show ) {
331 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
332 }
333
334 this.useragentMandatory = settings.useragentCheckbox.mandatory;
335 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
336
337 // HACK: Setting a link in the messages doesn't work. There is already a report
338 // about this, and the bug report offers a somewhat hacky work around that
339 // includes setting a separate message to be parsed.
340 // We want to make sure the user can configure both the title of the page and
341 // a separate url, so this must be allowed to parse correctly.
342 // See https://phabricator.wikimedia.org/T49395#490610
343 mw.messages.set( {
344 'feedback-dialog-temporary-message':
345 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
346 } );
347 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
348 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
349 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
350 this.feedbackMessageLabel.setLabel(
351 // Double-parse
352 $( '<span>' )
353 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
354 );
355
356 this.validateFeedbackForm();
357 }, this );
358 };
359
360 /**
361 * @inheritdoc
362 */
363 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
364 return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
365 .next( function () {
366 this.feedbackSubjectInput.focus();
367 }, this );
368 };
369
370 /**
371 * @inheritdoc
372 */
373 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
374 if ( action === 'cancel' ) {
375 return new OO.ui.Process( function () {
376 this.close( { action: action } );
377 }, this );
378 } else if ( action === 'external' ) {
379 return new OO.ui.Process( function () {
380 // Open in a new window
381 window.open( this.getBugReportLink(), '_blank' );
382 // Close the dialog
383 this.close();
384 }, this );
385 } else if ( action === 'submit' ) {
386 return new OO.ui.Process( function () {
387 var fb = this,
388 userAgentMessage = ':' +
389 '<small>' +
390 mw.msg( 'feedback-useragent' ) +
391 ' ' +
392 mw.html.escape( navigator.userAgent ) +
393 '</small>\n\n',
394 subject = this.feedbackSubjectInput.getValue(),
395 message = this.feedbackMessageInput.getValue();
396
397 // Add user agent if checkbox is selected
398 if ( this.useragentCheckbox.isSelected() ) {
399 message = userAgentMessage + message;
400 }
401
402 // Post the message
403 return this.messagePosterPromise.then( function ( poster ) {
404 return fb.postMessage( poster, subject, message );
405 }, function () {
406 fb.status = 'error4';
407 mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
408 } ).then( function () {
409 fb.close();
410 }, function () {
411 return fb.getErrorMessage();
412 } );
413 }, this );
414 }
415 // Fallback to parent handler
416 return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
417 };
418
419 /**
420 * Returns an error message for the current status.
421 *
422 * @private
423 *
424 * @return {OO.ui.Error}
425 */
426 mw.Feedback.Dialog.prototype.getErrorMessage = function () {
427 switch ( this.status ) {
428 case 'error1':
429 case 'error2':
430 case 'error3':
431 case 'error4':
432 // Messages: feedback-error1, feedback-error2, feedback-error3, feedback-error4
433 return new OO.ui.Error( mw.msg( 'feedback-' + this.status ) );
434 }
435 };
436
437 /**
438 * Posts the message
439 *
440 * @private
441 *
442 * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
443 * @param {string} subject Subject of message
444 * @param {string} message Body of message
445 * @return {jQuery.Promise} Promise representing success of message posting action
446 */
447 mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
448 var fb = this;
449
450 return poster.post(
451 subject,
452 message
453 ).then( function () {
454 fb.status = 'submitted';
455 }, function ( mainCode, secondaryCode, details ) {
456 if ( mainCode === 'api-fail' ) {
457 if ( secondaryCode === 'http' ) {
458 fb.status = 'error3';
459 // ajax request failed
460 mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
461 } else {
462 fb.status = 'error2';
463 mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
464 }
465 } else {
466 fb.status = 'error1';
467 }
468 } );
469 };
470
471 /**
472 * @inheritdoc
473 */
474 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
475 return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
476 .first( function () {
477 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
478 // Cleanup
479 this.status = '';
480 this.feedbackPageTitle = null;
481 this.feedbackSubjectInput.setValue( '' );
482 this.feedbackMessageInput.setValue( '' );
483 this.useragentCheckbox.setSelected( false );
484 }, this );
485 };
486
487 /**
488 * Set the bug report link
489 *
490 * @param {string} link Link to the external bug report form
491 */
492 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
493 this.bugReportLink = link;
494 };
495
496 /**
497 * Get the bug report link
498 *
499 * @return {string} Link to the external bug report form
500 */
501 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
502 return this.bugReportLink;
503 };
504
505 }( mediaWiki, jQuery ) );