Refactoring mw.Feedback to work with OOUI elements
[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 a combination of 'feedback-terms' and 'feedback-termsofuse' which includes a link to the
52 * wiki's Term of Use page.
53 */
54 mw.Feedback = function MwFeedback( config ) {
55 config = config || {};
56
57 this.api = config.api || new mw.Api();
58 this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
59
60 // Feedback page title
61 this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
62
63 // Links
64 this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/';
65 this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
66
67 // Terms of use
68 this.useragentCheckboxShow = !!config.showUseragentCheckbox;
69 this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory;
70 this.useragentCheckboxMessage = config.useragentCheckboxMessage ||
71 $( '<p>' )
72 .append( mw.msg( 'feedback-terms' ) )
73 .add( $( '<p>' ).append( mw.message( 'feedback-termsofuse' ).parse() ) );
74
75 // Message dialog
76 this.thankYouDialog = new OO.ui.MessageDialog();
77 };
78
79 /* Initialize */
80 OO.initClass( mw.Feedback );
81
82 /* Static Properties */
83 mw.Feedback.static.windowManager = null;
84 mw.Feedback.static.dialog = null;
85
86 /* Methods */
87
88 /**
89 * Respond to dialog submit event. If the information was
90 * submitted, either successfully or with an error, open
91 * a MessageDialog to thank the user.
92 * @param {string} [status] A status of the end of operation
93 * of the main feedback dialog. Empty if the dialog was
94 * dismissed with no action or the user followed the button
95 * to the external task reporting site.
96 */
97 mw.Feedback.prototype.onDialogSubmit = function ( status ) {
98 var dialogConfig = {};
99 switch ( status ) {
100 case 'submitted':
101 dialogConfig = {
102 title: mw.msg( 'feedback-thanks-title' ),
103 message: $( '<span>' ).append(
104 mw.message(
105 'feedback-thanks',
106 this.feedbackPageTitle.getNameText(),
107 $( '<a>' )
108 .attr( {
109 target: '_blank',
110 href: this.feedbackPageTitle.getUrl()
111 } )
112 ).parse()
113 ),
114 actions: [
115 {
116 action: 'accept',
117 label: mw.msg( 'feedback-close' ),
118 flags: 'primary'
119 }
120 ]
121 };
122 break;
123 case 'error1':
124 case 'error2':
125 case 'error3':
126 dialogConfig = {
127 title: mw.msg( 'feedback-error-title' ),
128 message: mw.msg( 'feedback-' + status ),
129 actions: [
130 {
131 action: 'accept',
132 label: mw.msg( 'feedback-close' ),
133 flags: 'primary'
134 }
135 ]
136 };
137 break;
138 }
139
140 // Show the message dialog
141 if ( !$.isEmptyObject( dialogConfig ) ) {
142 this.constructor.static.windowManager.openWindow(
143 this.thankYouDialog,
144 dialogConfig
145 );
146 }
147 };
148
149 /**
150 * Modify the display form, and then open it, focusing interface on the subject.
151 *
152 * @param {Object} [contents] Prefilled contents for the feedback form.
153 * @param {string} [contents.subject] The subject of the feedback
154 * @param {string} [contents.message] The content of the feedback
155 */
156 mw.Feedback.prototype.launch = function ( contents ) {
157 // Dialog
158 if ( !this.constructor.static.dialog ) {
159 this.constructor.static.dialog = new mw.Feedback.Dialog();
160 this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
161 }
162 if ( !this.constructor.static.windowManager ) {
163 this.constructor.static.windowManager = new OO.ui.WindowManager();
164 this.constructor.static.windowManager.addWindows( [
165 this.constructor.static.dialog,
166 this.thankYouDialog
167 ] );
168 $( 'body' )
169 .append( this.constructor.static.windowManager.$element );
170 }
171 // Open the dialog
172 this.constructor.static.windowManager.openWindow(
173 this.constructor.static.dialog,
174 {
175 title: mw.msg( this.dialogTitleMessageKey ),
176 settings: {
177 api: this.api,
178 title: this.feedbackPageTitle,
179 dialogTitleMessageKey: this.dialogTitleMessageKey,
180 bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
181 bugsTaskListLink: this.bugsTaskListLink,
182 useragentCheckbox: {
183 show: this.useragentCheckboxShow,
184 mandatory: this.useragentCheckboxMandatory,
185 message: this.useragentCheckboxMessage
186 }
187 },
188 contents: contents
189 }
190 );
191 };
192
193 /**
194 * mw.Feedback Dialog
195 *
196 * @class
197 * @extends OO.ui.ProcessDialog
198 *
199 * @constructor
200 * @param {Object} config Configuration object
201 */
202 mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
203 // Parent constructor
204 mw.Feedback.Dialog.super.call( this, config );
205
206 this.status = '';
207 this.feedbackPageTitle = null;
208 // Initialize
209 this.$element.addClass( 'mwFeedback-Dialog' );
210 };
211
212 OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
213
214 /* Static properties */
215 mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
216 mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
217 mw.Feedback.Dialog.static.size = 'medium';
218 mw.Feedback.Dialog.static.actions = [
219 {
220 action: 'submit',
221 label: mw.msg( 'feedback-submit' ),
222 flags: [ 'primary', 'constructive' ]
223 },
224 {
225 action: 'external',
226 label: mw.msg( 'feedback-external-bug-report-button' ),
227 flags: 'constructive'
228 },
229 {
230 action: 'cancel',
231 label: mw.msg( 'feedback-cancel' ),
232 flags: 'safe'
233 }
234 ];
235
236 /**
237 * @inheritdoc
238 */
239 mw.Feedback.Dialog.prototype.initialize = function () {
240 var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
241 feedbackFieldsetLayout;
242
243 // Parent method
244 mw.Feedback.Dialog.super.prototype.initialize.call( this );
245
246 this.feedbackPanel = new OO.ui.PanelLayout( {
247 scrollable: false,
248 expanded: false,
249 padded: true
250 } );
251
252 this.$spinner = $( '<div>' )
253 .addClass( 'feedback-spinner' );
254
255 // Feedback form
256 this.feedbackMessageLabel = new OO.ui.LabelWidget( {
257 classes: [ 'mw-feedbackDialog-welcome-message' ]
258 } );
259 this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
260 multiline: false
261 } );
262 this.feedbackMessageInput = new OO.ui.TextInputWidget( {
263 multiline: true
264 } );
265 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
266 label: mw.msg( 'feedback-subject' )
267 } );
268 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
269 label: mw.msg( 'feedback-message' )
270 } );
271 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
272 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
273 classes: [ 'mw-feedbackDialog-feedback-form' ]
274 } );
275
276 // Useragent terms of use
277 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
278 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
279 classes: [ 'mw-feedbackDialog-feedback-terms' ],
280 align: 'inline'
281 } );
282
283 this.feedbackPanel.$element.append(
284 this.feedbackMessageLabel.$element,
285 feedbackFieldsetLayout.$element,
286 this.useragentFieldLayout.$element
287 );
288
289 // Events
290 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
291 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
292 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
293
294 this.$body.append( this.feedbackPanel.$element );
295 };
296
297 /**
298 * Validate the feedback form
299 */
300 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
301 var isValid = (
302 (
303 !this.useragentMandatory ||
304 this.useragentCheckbox.isSelected()
305 ) &&
306 (
307 !!this.feedbackMessageInput.getValue() ||
308 !!this.feedbackSubjectInput.getValue()
309 )
310 );
311
312 this.actions.setAbilities( { submit: isValid } );
313 };
314
315 /**
316 * @inheritdoc
317 */
318 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
319 return this.feedbackPanel.$element.outerHeight( true );
320 };
321
322 /**
323 * @inheritdoc
324 */
325 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
326 return mw.Feedback.Dialog.super.prototype.getSetupProcess.call( this, data )
327 .next( function () {
328 var plainMsg, parsedMsg,
329 settings = data.settings;
330 data.contents = data.contents || {};
331
332 // Prefill subject/message
333 this.feedbackSubjectInput.setValue( data.contents.subject );
334 this.feedbackMessageInput.setValue( data.contents.message );
335
336 this.status = '';
337 this.api = settings.api;
338 this.setBugReportLink( settings.bugsTaskSubmissionLink );
339 this.feedbackPageTitle = settings.title;
340 this.feedbackPageName = settings.title.getNameText();
341 this.feedbackPageUrl = settings.title.getUrl();
342
343 // Useragent checkbox
344 if ( settings.useragentCheckbox.show ) {
345 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
346 }
347 this.useragentMandatory = settings.useragentCheckbox.mandatory;
348 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
349
350 // HACK: Setting a link in the messages doesn't work. There is already a report
351 // about this, and the bug report offers a somewhat hacky work around that
352 // includes setting a separate message to be parsed.
353 // We want to make sure the user can configure both the title of the page and
354 // a separate url, so this must be allowed to parse correctly.
355 // See https://phabricator.wikimedia.org/T49395#490610
356 mw.messages.set( {
357 'feedback-dialog-temporary-message':
358 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
359 } );
360 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
361 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
362 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
363 this.feedbackMessageLabel.setLabel(
364 // Double-parse
365 $( '<span>' )
366 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
367 );
368
369 this.validateFeedbackForm();
370 }, this );
371 };
372
373 /**
374 * @inheritdoc
375 */
376 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
377 return mw.Feedback.Dialog.super.prototype.getReadyProcess.call( this, data )
378 .next( function () {
379 this.feedbackSubjectInput.focus();
380 }, this );
381 };
382
383 /**
384 * @inheritdoc
385 */
386 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
387 if ( action === 'cancel' ) {
388 return new OO.ui.Process( function () {
389 this.close( { action: action } );
390 }, this );
391 } else if ( action === 'external' ) {
392 return new OO.ui.Process( function () {
393 // Open in a new window
394 window.open( this.getBugReportLink(), '_blank' );
395 // Close the dialog
396 this.close();
397 }, this );
398 } else if ( action === 'submit' ) {
399 return new OO.ui.Process( function () {
400 var fb = this,
401 userAgentMessage = ':' +
402 '<small>' +
403 mw.msg( 'feedback-useragent' ) +
404 ' ' +
405 mw.html.escape( navigator.userAgent ) +
406 '</small>\n\n',
407 subject = this.feedbackSubjectInput.getValue(),
408 message = this.feedbackMessageInput.getValue();
409
410 // Add user agent if checkbox is selected
411 if ( this.useragentCheckbox.isSelected() ) {
412 message = userAgentMessage + message;
413 }
414
415 // Add signature if needed
416 if ( message.indexOf( '~~~' ) === -1 ) {
417 message += '\n\n~~~~';
418 }
419
420 // Post the message, resolving redirects
421 this.pushPending();
422 this.api.newSection(
423 this.feedbackPageTitle,
424 subject,
425 message,
426 { redirect: true }
427 )
428 .done( function ( result ) {
429 if ( result.edit.result === 'Success' ) {
430 fb.status = 'submitted';
431 } else {
432 fb.status = 'error1';
433 }
434 fb.popPending();
435 fb.close();
436 } )
437 .fail( function ( code, result ) {
438 if ( code === 'http' ) {
439 fb.status = 'error3';
440 // ajax request failed
441 mw.log.warn( 'Feedback report failed with HTTP error: ' + result.textStatus );
442 } else {
443 fb.status = 'error2';
444 mw.log.warn( 'Feedback report failed with API error: ' + code );
445 }
446 fb.popPending();
447 fb.close();
448 } );
449 }, this );
450 }
451 // Fallback to parent handler
452 return mw.Feedback.Dialog.super.prototype.getActionProcess.call( this, action );
453 };
454
455 /**
456 * @inheritdoc
457 */
458 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
459 return mw.Feedback.Dialog.super.prototype.getTeardownProcess.call( this, data )
460 .first( function () {
461 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
462 // Cleanup
463 this.status = '';
464 this.feedbackPageTitle = null;
465 this.feedbackSubjectInput.setValue( '' );
466 this.feedbackMessageInput.setValue( '' );
467 this.useragentCheckbox.setSelected( false );
468 }, this );
469 };
470
471 /**
472 * Set the bug report link
473 * @param {string} link Link to the external bug report form
474 */
475 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
476 this.bugReportLink = link;
477 };
478
479 /**
480 * Get the bug report link
481 * @returns {string} Link to the external bug report form
482 */
483 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
484 return this.bugReportLink;
485 };
486
487 }( mediaWiki, jQuery ) );