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