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