Merge "Accessor to get EditPage parent revision ID"
[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 any content model that defines a
26 * `mw.messagePoster.MessagePoster`.
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.Title} [title="Feedback"] The title of the page where you collect
40 * feedback.
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/create/"] 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 );
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 *
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 case 'error1':
119 case 'error2':
120 case 'error3':
121 case 'error4':
122 dialogConfig = {
123 title: mw.msg( 'feedback-error-title' ),
124 message: mw.msg( 'feedback-' + status ),
125 actions: [
126 {
127 action: 'accept',
128 label: mw.msg( 'feedback-close' ),
129 flags: 'primary'
130 }
131 ]
132 };
133 break;
134 }
135
136 // Show the message dialog
137 if ( !$.isEmptyObject( dialogConfig ) ) {
138 this.constructor.static.windowManager.openWindow(
139 this.thankYouDialog,
140 dialogConfig
141 );
142 }
143 };
144
145 /**
146 * Modify the display form, and then open it, focusing interface on the subject.
147 *
148 * @param {Object} [contents] Prefilled contents for the feedback form.
149 * @param {string} [contents.subject] The subject of the feedback, as plaintext
150 * @param {string} [contents.message] The content of the feedback, as wikitext
151 */
152 mw.Feedback.prototype.launch = function ( contents ) {
153 // Dialog
154 if ( !this.constructor.static.dialog ) {
155 this.constructor.static.dialog = new mw.Feedback.Dialog();
156 this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
157 }
158 if ( !this.constructor.static.windowManager ) {
159 this.constructor.static.windowManager = new OO.ui.WindowManager();
160 this.constructor.static.windowManager.addWindows( [
161 this.constructor.static.dialog,
162 this.thankYouDialog
163 ] );
164 $( 'body' )
165 .append( this.constructor.static.windowManager.$element );
166 }
167 // Open the dialog
168 this.constructor.static.windowManager.openWindow(
169 this.constructor.static.dialog,
170 {
171 title: mw.msg( this.dialogTitleMessageKey ),
172 settings: {
173 messagePosterPromise: this.messagePosterPromise,
174 title: this.feedbackPageTitle,
175 dialogTitleMessageKey: this.dialogTitleMessageKey,
176 bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
177 bugsTaskListLink: this.bugsTaskListLink,
178 useragentCheckbox: {
179 show: this.useragentCheckboxShow,
180 mandatory: this.useragentCheckboxMandatory,
181 message: this.useragentCheckboxMessage
182 }
183 },
184 contents: contents
185 }
186 );
187 };
188
189 /**
190 * mw.Feedback Dialog
191 *
192 * @class
193 * @extends OO.ui.ProcessDialog
194 *
195 * @constructor
196 * @param {Object} config Configuration object
197 */
198 mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
199 // Parent constructor
200 mw.Feedback.Dialog.parent.call( this, config );
201
202 this.status = '';
203 this.feedbackPageTitle = null;
204 // Initialize
205 this.$element.addClass( 'mwFeedback-Dialog' );
206 };
207
208 OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
209
210 /* Static properties */
211 mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
212 mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
213 mw.Feedback.Dialog.static.size = 'medium';
214 mw.Feedback.Dialog.static.actions = [
215 {
216 action: 'submit',
217 label: mw.msg( 'feedback-submit' ),
218 flags: [ 'primary', 'constructive' ]
219 },
220 {
221 action: 'external',
222 label: mw.msg( 'feedback-external-bug-report-button' ),
223 flags: 'constructive'
224 },
225 {
226 action: 'cancel',
227 label: mw.msg( 'feedback-cancel' ),
228 flags: 'safe'
229 }
230 ];
231
232 /**
233 * @inheritdoc
234 */
235 mw.Feedback.Dialog.prototype.initialize = function () {
236 var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
237 feedbackFieldsetLayout, termsOfUseLabel;
238
239 // Parent method
240 mw.Feedback.Dialog.parent.prototype.initialize.call( this );
241
242 this.feedbackPanel = new OO.ui.PanelLayout( {
243 scrollable: false,
244 expanded: false,
245 padded: true
246 } );
247
248 this.$spinner = $( '<div>' )
249 .addClass( 'feedback-spinner' );
250
251 // Feedback form
252 this.feedbackMessageLabel = new OO.ui.LabelWidget( {
253 classes: [ 'mw-feedbackDialog-welcome-message' ]
254 } );
255 this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
256 multiline: false
257 } );
258 this.feedbackMessageInput = new OO.ui.TextInputWidget( {
259 autosize: true,
260 multiline: true
261 } );
262 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
263 label: mw.msg( 'feedback-subject' )
264 } );
265 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
266 label: mw.msg( 'feedback-message' )
267 } );
268 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
269 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
270 classes: [ 'mw-feedbackDialog-feedback-form' ]
271 } );
272
273 // Useragent terms of use
274 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
275 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
276 classes: [ 'mw-feedbackDialog-feedback-terms' ],
277 align: 'inline'
278 } );
279
280 termsOfUseLabel = new OO.ui.LabelWidget( {
281 classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
282 label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
283 } );
284
285 this.feedbackPanel.$element.append(
286 this.feedbackMessageLabel.$element,
287 feedbackFieldsetLayout.$element,
288 this.useragentFieldLayout.$element,
289 termsOfUseLabel.$element
290 );
291
292 // Events
293 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
294 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
295 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
296 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
297
298 this.$body.append( this.feedbackPanel.$element );
299 };
300
301 /**
302 * Validate the feedback form
303 */
304 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
305 var isValid = (
306 (
307 !this.useragentMandatory ||
308 this.useragentCheckbox.isSelected()
309 ) &&
310 (
311 !!this.feedbackMessageInput.getValue() ||
312 !!this.feedbackSubjectInput.getValue()
313 )
314 );
315
316 this.actions.setAbilities( { submit: isValid } );
317 };
318
319 /**
320 * @inheritdoc
321 */
322 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
323 return this.feedbackPanel.$element.outerHeight( true );
324 };
325
326 /**
327 * @inheritdoc
328 */
329 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
330 return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
331 .next( function () {
332 var plainMsg, parsedMsg,
333 settings = data.settings;
334 data.contents = data.contents || {};
335
336 // Prefill subject/message
337 this.feedbackSubjectInput.setValue( data.contents.subject );
338 this.feedbackMessageInput.setValue( data.contents.message );
339
340 this.status = '';
341 this.messagePosterPromise = settings.messagePosterPromise;
342 this.setBugReportLink( settings.bugsTaskSubmissionLink );
343 this.feedbackPageTitle = settings.title;
344 this.feedbackPageName = settings.title.getNameText();
345 this.feedbackPageUrl = settings.title.getUrl();
346
347 // Useragent checkbox
348 if ( settings.useragentCheckbox.show ) {
349 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
350 }
351
352 this.useragentMandatory = settings.useragentCheckbox.mandatory;
353 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
354
355 // HACK: Setting a link in the messages doesn't work. There is already a report
356 // about this, and the bug report offers a somewhat hacky work around that
357 // includes setting a separate message to be parsed.
358 // We want to make sure the user can configure both the title of the page and
359 // a separate url, so this must be allowed to parse correctly.
360 // See https://phabricator.wikimedia.org/T49395#490610
361 mw.messages.set( {
362 'feedback-dialog-temporary-message':
363 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
364 } );
365 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
366 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
367 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
368 this.feedbackMessageLabel.setLabel(
369 // Double-parse
370 $( '<span>' )
371 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
372 );
373
374 this.validateFeedbackForm();
375 }, this );
376 };
377
378 /**
379 * @inheritdoc
380 */
381 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
382 return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
383 .next( function () {
384 this.feedbackSubjectInput.focus();
385 }, this );
386 };
387
388 /**
389 * @inheritdoc
390 */
391 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
392 if ( action === 'cancel' ) {
393 return new OO.ui.Process( function () {
394 this.close( { action: action } );
395 }, this );
396 } else if ( action === 'external' ) {
397 return new OO.ui.Process( function () {
398 // Open in a new window
399 window.open( this.getBugReportLink(), '_blank' );
400 // Close the dialog
401 this.close();
402 }, this );
403 } else if ( action === 'submit' ) {
404 return new OO.ui.Process( function () {
405 var fb = this,
406 userAgentMessage = ':' +
407 '<small>' +
408 mw.msg( 'feedback-useragent' ) +
409 ' ' +
410 mw.html.escape( navigator.userAgent ) +
411 '</small>\n\n',
412 subject = this.feedbackSubjectInput.getValue(),
413 message = this.feedbackMessageInput.getValue();
414
415 // Add user agent if checkbox is selected
416 if ( this.useragentCheckbox.isSelected() ) {
417 message = userAgentMessage + message;
418 }
419
420 // Post the message
421 return this.messagePosterPromise.then( function ( poster ) {
422 return fb.postMessage( poster, subject, message );
423 }, function () {
424 fb.status = 'error4';
425 mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
426 } ).always( function () {
427 fb.close();
428 } );
429 }, this );
430 }
431 // Fallback to parent handler
432 return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
433 };
434
435 /**
436 * Posts the message
437 *
438 * @private
439 *
440 * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
441 * @param {string} subject Subject of message
442 * @param {string} message Body of message
443 * @return {jQuery.Promise} Promise representing success of message posting action
444 */
445 mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
446 var fb = this;
447
448 return poster.post(
449 subject,
450 message
451 ).then( function () {
452 fb.status = 'submitted';
453 }, function ( mainCode, secondaryCode, details ) {
454 if ( mainCode === 'api-fail' ) {
455 if ( secondaryCode === 'http' ) {
456 fb.status = 'error3';
457 // ajax request failed
458 mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
459 } else {
460 fb.status = 'error2';
461 mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
462 }
463 } else {
464 fb.status = 'error1';
465 }
466 } );
467 };
468
469 /**
470 * @inheritdoc
471 */
472 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
473 return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
474 .first( function () {
475 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
476 // Cleanup
477 this.status = '';
478 this.feedbackPageTitle = null;
479 this.feedbackSubjectInput.setValue( '' );
480 this.feedbackMessageInput.setValue( '' );
481 this.useragentCheckbox.setSelected( false );
482 }, this );
483 };
484
485 /**
486 * Set the bug report link
487 *
488 * @param {string} link Link to the external bug report form
489 */
490 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
491 this.bugReportLink = link;
492 };
493
494 /**
495 * Get the bug report link
496 *
497 * @return {string} Link to the external bug report form
498 */
499 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
500 return this.bugReportLink;
501 };
502
503 }( mediaWiki, jQuery ) );