Merge "Shorten long lines in PrefixSearch.php to make phpcs pass"
[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 autosize: true,
264 multiline: true
265 } );
266 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
267 label: mw.msg( 'feedback-subject' )
268 } );
269 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
270 label: mw.msg( 'feedback-message' )
271 } );
272 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
273 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
274 classes: [ 'mw-feedbackDialog-feedback-form' ]
275 } );
276
277 // Useragent terms of use
278 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
279 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
280 classes: [ 'mw-feedbackDialog-feedback-terms' ],
281 align: 'inline'
282 } );
283
284 this.feedbackPanel.$element.append(
285 this.feedbackMessageLabel.$element,
286 feedbackFieldsetLayout.$element,
287 this.useragentFieldLayout.$element
288 );
289
290 // Events
291 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
292 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
293 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
294 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
295
296 this.$body.append( this.feedbackPanel.$element );
297 };
298
299 /**
300 * Validate the feedback form
301 */
302 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
303 var isValid = (
304 (
305 !this.useragentMandatory ||
306 this.useragentCheckbox.isSelected()
307 ) &&
308 (
309 !!this.feedbackMessageInput.getValue() ||
310 !!this.feedbackSubjectInput.getValue()
311 )
312 );
313
314 this.actions.setAbilities( { submit: isValid } );
315 };
316
317 /**
318 * @inheritdoc
319 */
320 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
321 return this.feedbackPanel.$element.outerHeight( true );
322 };
323
324 /**
325 * @inheritdoc
326 */
327 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
328 return mw.Feedback.Dialog.super.prototype.getSetupProcess.call( this, data )
329 .next( function () {
330 var plainMsg, parsedMsg,
331 settings = data.settings;
332 data.contents = data.contents || {};
333
334 // Prefill subject/message
335 this.feedbackSubjectInput.setValue( data.contents.subject );
336 this.feedbackMessageInput.setValue( data.contents.message );
337
338 this.status = '';
339 this.api = settings.api;
340 this.setBugReportLink( settings.bugsTaskSubmissionLink );
341 this.feedbackPageTitle = settings.title;
342 this.feedbackPageName = settings.title.getNameText();
343 this.feedbackPageUrl = settings.title.getUrl();
344
345 // Useragent checkbox
346 if ( settings.useragentCheckbox.show ) {
347 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
348 }
349 this.useragentMandatory = settings.useragentCheckbox.mandatory;
350 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
351
352 // HACK: Setting a link in the messages doesn't work. There is already a report
353 // about this, and the bug report offers a somewhat hacky work around that
354 // includes setting a separate message to be parsed.
355 // We want to make sure the user can configure both the title of the page and
356 // a separate url, so this must be allowed to parse correctly.
357 // See https://phabricator.wikimedia.org/T49395#490610
358 mw.messages.set( {
359 'feedback-dialog-temporary-message':
360 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
361 } );
362 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
363 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
364 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
365 this.feedbackMessageLabel.setLabel(
366 // Double-parse
367 $( '<span>' )
368 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
369 );
370
371 this.validateFeedbackForm();
372 }, this );
373 };
374
375 /**
376 * @inheritdoc
377 */
378 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
379 return mw.Feedback.Dialog.super.prototype.getReadyProcess.call( this, data )
380 .next( function () {
381 this.feedbackSubjectInput.focus();
382 }, this );
383 };
384
385 /**
386 * @inheritdoc
387 */
388 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
389 if ( action === 'cancel' ) {
390 return new OO.ui.Process( function () {
391 this.close( { action: action } );
392 }, this );
393 } else if ( action === 'external' ) {
394 return new OO.ui.Process( function () {
395 // Open in a new window
396 window.open( this.getBugReportLink(), '_blank' );
397 // Close the dialog
398 this.close();
399 }, this );
400 } else if ( action === 'submit' ) {
401 return new OO.ui.Process( function () {
402 var fb = this,
403 userAgentMessage = ':' +
404 '<small>' +
405 mw.msg( 'feedback-useragent' ) +
406 ' ' +
407 mw.html.escape( navigator.userAgent ) +
408 '</small>\n\n',
409 subject = this.feedbackSubjectInput.getValue(),
410 message = this.feedbackMessageInput.getValue();
411
412 // Add user agent if checkbox is selected
413 if ( this.useragentCheckbox.isSelected() ) {
414 message = userAgentMessage + message;
415 }
416
417 // Add signature if needed
418 if ( message.indexOf( '~~~' ) === -1 ) {
419 message += '\n\n~~~~';
420 }
421
422 // Post the message, resolving redirects
423 this.pushPending();
424 this.api.newSection(
425 this.feedbackPageTitle,
426 subject,
427 message,
428 { redirect: true }
429 )
430 .done( function ( result ) {
431 if ( result.edit.result === 'Success' ) {
432 fb.status = 'submitted';
433 } else {
434 fb.status = 'error1';
435 }
436 fb.popPending();
437 fb.close();
438 } )
439 .fail( function ( code, result ) {
440 if ( code === 'http' ) {
441 fb.status = 'error3';
442 // ajax request failed
443 mw.log.warn( 'Feedback report failed with HTTP error: ' + result.textStatus );
444 } else {
445 fb.status = 'error2';
446 mw.log.warn( 'Feedback report failed with API error: ' + code );
447 }
448 fb.popPending();
449 fb.close();
450 } );
451 }, this );
452 }
453 // Fallback to parent handler
454 return mw.Feedback.Dialog.super.prototype.getActionProcess.call( this, action );
455 };
456
457 /**
458 * @inheritdoc
459 */
460 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
461 return mw.Feedback.Dialog.super.prototype.getTeardownProcess.call( this, data )
462 .first( function () {
463 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
464 // Cleanup
465 this.status = '';
466 this.feedbackPageTitle = null;
467 this.feedbackSubjectInput.setValue( '' );
468 this.feedbackMessageInput.setValue( '' );
469 this.useragentCheckbox.setSelected( false );
470 }, this );
471 };
472
473 /**
474 * Set the bug report link
475 * @param {string} link Link to the external bug report form
476 */
477 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
478 this.bugReportLink = link;
479 };
480
481 /**
482 * Get the bug report link
483 * @returns {string} Link to the external bug report form
484 */
485 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
486 return this.bugReportLink;
487 };
488
489 }( mediaWiki, jQuery ) );