Merge "mw.htmlform: Fix hiding of the textbox for 'selectorother' fields on page...
[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 ( 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 || {};
53
54 this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
55
56 // Feedback page title
57 this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
58
59 this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle, config.apiUrl );
60
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';
64
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' ) );
70
71 // Message dialog
72 this.thankYouDialog = new OO.ui.MessageDialog();
73 };
74
75 /* Initialize */
76 OO.initClass( mw.Feedback );
77
78 /* Static Properties */
79 mw.Feedback.static.windowManager = null;
80 mw.Feedback.static.dialog = null;
81
82 /* Methods */
83
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 }
118
119 // Show the message dialog
120 if ( !$.isEmptyObject( dialogConfig ) ) {
121 this.constructor.static.windowManager.openWindow(
122 this.thankYouDialog,
123 dialogConfig
124 );
125 }
126 };
127
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 };
171
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 );
184
185 this.status = '';
186 this.feedbackPageTitle = null;
187 // Initialize
188 this.$element.addClass( 'mwFeedback-Dialog' );
189 };
190
191 OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
192
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 ];
214
215 /**
216 * @inheritdoc
217 */
218 mw.Feedback.Dialog.prototype.initialize = function () {
219 var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
220 feedbackFieldsetLayout, termsOfUseLabel;
221
222 // Parent method
223 mw.Feedback.Dialog.parent.prototype.initialize.call( this );
224
225 this.feedbackPanel = new OO.ui.PanelLayout( {
226 scrollable: false,
227 expanded: false,
228 padded: true
229 } );
230
231 this.$spinner = $( '<div>' )
232 .addClass( 'feedback-spinner' );
233
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 } );
241 this.feedbackMessageInput = new OO.ui.MultilineTextInputWidget( {
242 autosize: true
243 } );
244 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
245 label: mw.msg( 'feedback-subject' )
246 } );
247 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
248 label: mw.msg( 'feedback-message' )
249 } );
250 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
251 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
252 classes: [ 'mw-feedbackDialog-feedback-form' ]
253 } );
254
255 // Useragent terms of use
256 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
257 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
258 classes: [ 'mw-feedbackDialog-feedback-terms' ],
259 align: 'inline'
260 } );
261
262 termsOfUseLabel = new OO.ui.LabelWidget( {
263 classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
264 label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
265 } );
266
267 this.feedbackPanel.$element.append(
268 this.feedbackMessageLabel.$element,
269 feedbackFieldsetLayout.$element,
270 this.useragentFieldLayout.$element,
271 termsOfUseLabel.$element
272 );
273
274 // Events
275 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
276 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
277 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
278 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
279
280 this.$body.append( this.feedbackPanel.$element );
281 };
282
283 /**
284 * Validate the feedback form
285 */
286 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
287 var isValid = (
288 (
289 !this.useragentMandatory ||
290 this.useragentCheckbox.isSelected()
291 ) &&
292 this.feedbackSubjectInput.getValue()
293 );
294
295 this.actions.setAbilities( { submit: isValid } );
296 };
297
298 /**
299 * @inheritdoc
300 */
301 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
302 return this.feedbackPanel.$element.outerHeight( true );
303 };
304
305 /**
306 * @inheritdoc
307 */
308 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
309 return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
310 .next( function () {
311 var plainMsg, parsedMsg,
312 settings = data.settings;
313 data.contents = data.contents || {};
314
315 // Prefill subject/message
316 this.feedbackSubjectInput.setValue( data.contents.subject );
317 this.feedbackMessageInput.setValue( data.contents.message );
318
319 this.status = '';
320 this.messagePosterPromise = settings.messagePosterPromise;
321 this.setBugReportLink( settings.bugsTaskSubmissionLink );
322 this.feedbackPageTitle = settings.title;
323 this.feedbackPageName = settings.title.getNameText();
324 this.feedbackPageUrl = settings.title.getUrl();
325
326 // Useragent checkbox
327 if ( settings.useragentCheckbox.show ) {
328 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
329 }
330
331 this.useragentMandatory = settings.useragentCheckbox.mandatory;
332 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
333
334 // HACK: Setting a link in the messages doesn't work. There is already a report
335 // about this, and the bug report offers a somewhat hacky work around that
336 // includes setting a separate message to be parsed.
337 // We want to make sure the user can configure both the title of the page and
338 // a separate url, so this must be allowed to parse correctly.
339 // See https://phabricator.wikimedia.org/T49395#490610
340 mw.messages.set( {
341 'feedback-dialog-temporary-message':
342 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
343 } );
344 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
345 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
346 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
347 this.feedbackMessageLabel.setLabel(
348 // Double-parse
349 $( '<span>' )
350 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
351 );
352
353 this.validateFeedbackForm();
354 }, this );
355 };
356
357 /**
358 * @inheritdoc
359 */
360 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
361 return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
362 .next( function () {
363 this.feedbackSubjectInput.focus();
364 }, this );
365 };
366
367 /**
368 * @inheritdoc
369 */
370 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
371 if ( action === 'cancel' ) {
372 return new OO.ui.Process( function () {
373 this.close( { action: action } );
374 }, this );
375 } else if ( action === 'external' ) {
376 return new OO.ui.Process( function () {
377 // Open in a new window
378 window.open( this.getBugReportLink(), '_blank' );
379 // Close the dialog
380 this.close();
381 }, this );
382 } else if ( action === 'submit' ) {
383 return new OO.ui.Process( function () {
384 var fb = this,
385 userAgentMessage = ':' +
386 '<small>' +
387 mw.msg( 'feedback-useragent' ) +
388 ' ' +
389 mw.html.escape( navigator.userAgent ) +
390 '</small>\n\n',
391 subject = this.feedbackSubjectInput.getValue(),
392 message = this.feedbackMessageInput.getValue();
393
394 // Add user agent if checkbox is selected
395 if ( this.useragentCheckbox.isSelected() ) {
396 message = userAgentMessage + message;
397 }
398
399 // Post the message
400 return this.messagePosterPromise.then( function ( poster ) {
401 return fb.postMessage( poster, subject, message );
402 }, function () {
403 fb.status = 'error4';
404 mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
405 } ).then( function () {
406 fb.close();
407 }, function () {
408 return fb.getErrorMessage();
409 } );
410 }, this );
411 }
412 // Fallback to parent handler
413 return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
414 };
415
416 /**
417 * Returns an error message for the current status.
418 *
419 * @private
420 *
421 * @return {OO.ui.Error}
422 */
423 mw.Feedback.Dialog.prototype.getErrorMessage = function () {
424 switch ( this.status ) {
425 case 'error1':
426 case 'error2':
427 case 'error3':
428 case 'error4':
429 // Messages: feedback-error1, feedback-error2, feedback-error3, feedback-error4
430 return new OO.ui.Error( mw.msg( 'feedback-' + this.status ) );
431 }
432 };
433
434 /**
435 * Posts the message
436 *
437 * @private
438 *
439 * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
440 * @param {string} subject Subject of message
441 * @param {string} message Body of message
442 * @return {jQuery.Promise} Promise representing success of message posting action
443 */
444 mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
445 var fb = this;
446
447 return poster.post(
448 subject,
449 message
450 ).then( function () {
451 fb.status = 'submitted';
452 }, function ( mainCode, secondaryCode, details ) {
453 if ( mainCode === 'api-fail' ) {
454 if ( secondaryCode === 'http' ) {
455 fb.status = 'error3';
456 // ajax request failed
457 mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
458 } else {
459 fb.status = 'error2';
460 mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
461 }
462 } else {
463 fb.status = 'error1';
464 }
465 } );
466 };
467
468 /**
469 * @inheritdoc
470 */
471 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
472 return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
473 .first( function () {
474 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
475 // Cleanup
476 this.status = '';
477 this.feedbackPageTitle = null;
478 this.feedbackSubjectInput.setValue( '' );
479 this.feedbackMessageInput.setValue( '' );
480 this.useragentCheckbox.setSelected( false );
481 }, this );
482 };
483
484 /**
485 * Set the bug report link
486 *
487 * @param {string} link Link to the external bug report form
488 */
489 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
490 this.bugReportLink = link;
491 };
492
493 /**
494 * Get the bug report link
495 *
496 * @return {string} Link to the external bug report form
497 */
498 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
499 return this.bugReportLink;
500 };
501
502 }( mediaWiki, jQuery ) );