diff options
Diffstat (limited to 'resources/src/mediawiki/mediawiki.feedback.js')
-rw-r--r-- | resources/src/mediawiki/mediawiki.feedback.js | 699 |
1 files changed, 441 insertions, 258 deletions
diff --git a/resources/src/mediawiki/mediawiki.feedback.js b/resources/src/mediawiki/mediawiki.feedback.js index 1c0d8332..d9401001 100644 --- a/resources/src/mediawiki/mediawiki.feedback.js +++ b/resources/src/mediawiki/mediawiki.feedback.js @@ -3,8 +3,11 @@ * * @author Ryan Kaldari, 2010 * @author Neil Kandalgaonkar, 2010-11 + * @author Moriel Schottlender, 2015 * @since 1.19 */ +/*jshint es3:false */ +/*global OO*/ ( function ( mw, $ ) { /** * This is a way of getting simple feedback from users. It's useful @@ -32,289 +35,469 @@ * * @class * @constructor - * @param {Object} [options] - * @param {mw.Api} [options.api] if omitted, will just create a standard API - * @param {mw.Title} [options.title="Feedback"] The title of the page where you collect - * feedback. - * @param {string} [options.dialogTitleMessageKey="feedback-submit"] Message key for the - * title of the dialog box - * @param {string} [options.bugsLink="//bugzilla.wikimedia.org/enter_bug.cgi"] URL where - * bugs can be posted - * @param {mw.Uri|string} [options.bugsListLink="//bugzilla.wikimedia.org/query.cgi"] - * URL where bugs can be listed + * @param {Object} [config] Configuration object + * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect + * feedback. + * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the + * title of the dialog box + * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/create/"] URL where + * bugs can be posted + * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL + * where bugs can be listed + * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form. + * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory. + * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox. + * defaults to the message 'feedback-terms'. */ - mw.Feedback = function ( options ) { - if ( options === undefined ) { - options = {}; - } + mw.Feedback = function MwFeedback( config ) { + config = config || {}; - if ( options.api === undefined ) { - options.api = new mw.Api(); - } + this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title'; - if ( options.title === undefined ) { - options.title = new mw.Title( 'Feedback' ); - } + // Feedback page title + this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' ); - if ( options.dialogTitleMessageKey === undefined ) { - options.dialogTitleMessageKey = 'feedback-submit'; - } + this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle ); - if ( options.bugsLink === undefined ) { - options.bugsLink = '//bugzilla.wikimedia.org/enter_bug.cgi'; - } + // Links + this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/'; + this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced'; - if ( options.bugsListLink === undefined ) { - options.bugsListLink = '//bugzilla.wikimedia.org/query.cgi'; - } + // Terms of use + this.useragentCheckboxShow = !!config.showUseragentCheckbox; + this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory; + this.useragentCheckboxMessage = config.useragentCheckboxMessage || + $( '<p>' ).append( mw.msg( 'feedback-terms' ) ); - $.extend( this, options ); - this.setup(); + // Message dialog + this.thankYouDialog = new OO.ui.MessageDialog(); }; - mw.Feedback.prototype = { - /** - * Sets up interface - */ - setup: function () { - var $feedbackPageLink, - $bugNoteLink, - $bugsListLink, - fb = this; - - $feedbackPageLink = $( '<a>' ) - .attr( { - href: fb.title.getUrl(), - target: '_blank' - } ) - .css( { - whiteSpace: 'nowrap' - } ); + /* Initialize */ + OO.initClass( mw.Feedback ); - $bugNoteLink = $( '<a>' ).attr( { href: '#' } ).click( function () { - fb.displayBugs(); - } ); - - $bugsListLink = $( '<a>' ).attr( { - href: fb.bugsListLink, - target: '_blank' - } ); - - // TODO: Use a stylesheet instead of these inline styles - this.$dialog = - $( '<div style="position: relative;"></div>' ).append( - $( '<div class="feedback-mode feedback-form"></div>' ).append( - $( '<small>' ).append( - $( '<p>' ).msg( - 'feedback-bugornote', - $bugNoteLink, - fb.title.getNameText(), - $feedbackPageLink.clone() - ) - ), - $( '<div style="margin-top: 1em;"></div>' ) - .msg( 'feedback-subject' ) - .append( - $( '<br>' ), - $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;"/>' ) - ), - $( '<div style="margin-top: 0.4em;"></div>' ) - .msg( 'feedback-message' ) - .append( - $( '<br>' ), - $( '<textarea name="message" class="feedback-message" rows="5" cols="60"></textarea>' ) - ) - ), - $( '<div class="feedback-mode feedback-bugs"></div>' ).append( - $( '<p>' ).msg( 'feedback-bugcheck', $bugsListLink ) - ), - $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' ) - .msg( 'feedback-adding' ) - .append( - $( '<br>' ), - $( '<span class="feedback-spinner"></span>' ) - ), - $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg( - 'feedback-thanks', fb.title.getNameText(), $feedbackPageLink.clone() + /* Static Properties */ + mw.Feedback.static.windowManager = null; + mw.Feedback.static.dialog = null; + + /* Methods */ + + /** + * Respond to dialog submit event. If the information was + * submitted, either successfully or with an error, open + * a MessageDialog to thank the user. + * @param {string} [status] A status of the end of operation + * of the main feedback dialog. Empty if the dialog was + * dismissed with no action or the user followed the button + * to the external task reporting site. + */ + mw.Feedback.prototype.onDialogSubmit = function ( status ) { + var dialogConfig = {}; + switch ( status ) { + case 'submitted': + dialogConfig = { + title: mw.msg( 'feedback-thanks-title' ), + message: $( '<span>' ).append( + mw.message( + 'feedback-thanks', + this.feedbackPageTitle.getNameText(), + $( '<a>' ) + .attr( { + target: '_blank', + href: this.feedbackPageTitle.getUrl() + } ) + ).parse() ), - $( '<div class="feedback-mode feedback-error" style="position: relative;"></div>' ).append( - $( '<div class="feedback-error-msg style="color: #990000; margin-top: 0.4em;"></div>' ) - ) - ); + actions: [ + { + action: 'accept', + label: mw.msg( 'feedback-close' ), + flags: 'primary' + } + ] + }; + break; + case 'error1': + case 'error2': + case 'error3': + case 'error4': + dialogConfig = { + title: mw.msg( 'feedback-error-title' ), + message: mw.msg( 'feedback-' + status ), + actions: [ + { + action: 'accept', + label: mw.msg( 'feedback-close' ), + flags: 'primary' + } + ] + }; + break; + } - this.$dialog.dialog( { - width: 500, - autoOpen: false, - title: mw.message( this.dialogTitleMessageKey ).escaped(), - modal: true, - buttons: fb.buttons - } ); + // Show the message dialog + if ( !$.isEmptyObject( dialogConfig ) ) { + this.constructor.static.windowManager.openWindow( + this.thankYouDialog, + dialogConfig + ); + } + }; - this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get( 0 ); - this.messageInput = this.$dialog.find( 'textarea.feedback-message' ).get( 0 ); - }, + /** + * Modify the display form, and then open it, focusing interface on the subject. + * + * @param {Object} [contents] Prefilled contents for the feedback form. + * @param {string} [contents.subject] The subject of the feedback, as plaintext + * @param {string} [contents.message] The content of the feedback, as wikitext + */ + mw.Feedback.prototype.launch = function ( contents ) { + // Dialog + if ( !this.constructor.static.dialog ) { + this.constructor.static.dialog = new mw.Feedback.Dialog(); + this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } ); + } + if ( !this.constructor.static.windowManager ) { + this.constructor.static.windowManager = new OO.ui.WindowManager(); + this.constructor.static.windowManager.addWindows( [ + this.constructor.static.dialog, + this.thankYouDialog + ] ); + $( 'body' ) + .append( this.constructor.static.windowManager.$element ); + } + // Open the dialog + this.constructor.static.windowManager.openWindow( + this.constructor.static.dialog, + { + title: mw.msg( this.dialogTitleMessageKey ), + settings: { + messagePosterPromise: this.messagePosterPromise, + title: this.feedbackPageTitle, + dialogTitleMessageKey: this.dialogTitleMessageKey, + bugsTaskSubmissionLink: this.bugsTaskSubmissionLink, + bugsTaskListLink: this.bugsTaskListLink, + useragentCheckbox: { + show: this.useragentCheckboxShow, + mandatory: this.useragentCheckboxMandatory, + message: this.useragentCheckboxMessage + } + }, + contents: contents + } + ); + }; - /** - * Displays a section of the dialog. - * - * @param {"form"|"bugs"|"submitting"|"thanks"|"error"} s - * The section of the dialog to show. - */ - display: function ( s ) { - // Hide the buttons - this.$dialog.dialog( { buttons: {} } ); - // Hide everything - this.$dialog.find( '.feedback-mode' ).hide(); - // Show the desired div - this.$dialog.find( '.feedback-' + s ).show(); - }, + /** + * mw.Feedback Dialog + * + * @class + * @extends OO.ui.ProcessDialog + * + * @constructor + * @param {Object} config Configuration object + */ + mw.Feedback.Dialog = function mwFeedbackDialog( config ) { + // Parent constructor + mw.Feedback.Dialog.super.call( this, config ); + + this.status = ''; + this.feedbackPageTitle = null; + // Initialize + this.$element.addClass( 'mwFeedback-Dialog' ); + }; - /** - * Display the submitting section. - */ - displaySubmitting: function () { - this.display( 'submitting' ); + OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog ); + + /* Static properties */ + mw.Feedback.Dialog.static.name = 'mwFeedbackDialog'; + mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' ); + mw.Feedback.Dialog.static.size = 'medium'; + mw.Feedback.Dialog.static.actions = [ + { + action: 'submit', + label: mw.msg( 'feedback-submit' ), + flags: [ 'primary', 'constructive' ] }, - - /** - * Display the bugs section. - */ - displayBugs: function () { - var fb = this, - bugsButtons = {}; - - this.display( 'bugs' ); - bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () { - window.open( fb.bugsLink, '_blank' ); - }; - bugsButtons[ mw.msg( 'feedback-cancel' ) ] = function () { - fb.cancel(); - }; - this.$dialog.dialog( { - buttons: bugsButtons - } ); + { + action: 'external', + label: mw.msg( 'feedback-external-bug-report-button' ), + flags: 'constructive' }, + { + action: 'cancel', + label: mw.msg( 'feedback-cancel' ), + flags: 'safe' + } + ]; - /** - * Display the thanks section. - */ - displayThanks: function () { - var fb = this, - closeButton = {}; - - this.display( 'thanks' ); - closeButton[ mw.msg( 'feedback-close' ) ] = function () { - fb.$dialog.dialog( 'close' ); - }; - this.$dialog.dialog( { - buttons: closeButton - } ); - }, + /** + * @inheritdoc + */ + mw.Feedback.Dialog.prototype.initialize = function () { + var feedbackSubjectFieldLayout, feedbackMessageFieldLayout, + feedbackFieldsetLayout, termsOfUseLabel; + + // Parent method + mw.Feedback.Dialog.super.prototype.initialize.call( this ); + + this.feedbackPanel = new OO.ui.PanelLayout( { + scrollable: false, + expanded: false, + padded: true + } ); + + this.$spinner = $( '<div>' ) + .addClass( 'feedback-spinner' ); + + // Feedback form + this.feedbackMessageLabel = new OO.ui.LabelWidget( { + classes: [ 'mw-feedbackDialog-welcome-message' ] + } ); + this.feedbackSubjectInput = new OO.ui.TextInputWidget( { + multiline: false + } ); + this.feedbackMessageInput = new OO.ui.TextInputWidget( { + autosize: true, + multiline: true + } ); + feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, { + label: mw.msg( 'feedback-subject' ) + } ); + feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, { + label: mw.msg( 'feedback-message' ) + } ); + feedbackFieldsetLayout = new OO.ui.FieldsetLayout( { + items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ], + classes: [ 'mw-feedbackDialog-feedback-form' ] + } ); + + // Useragent terms of use + this.useragentCheckbox = new OO.ui.CheckboxInputWidget(); + this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, { + classes: [ 'mw-feedbackDialog-feedback-terms' ], + align: 'inline' + } ); + + termsOfUseLabel = new OO.ui.LabelWidget( { + classes: [ 'mw-feedbackDialog-feedback-termsofuse' ], + label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) ) + } ); + + this.feedbackPanel.$element.append( + this.feedbackMessageLabel.$element, + feedbackFieldsetLayout.$element, + this.useragentFieldLayout.$element, + termsOfUseLabel.$element + ); + + // Events + this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } ); + this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } ); + this.feedbackMessageInput.connect( this, { change: 'updateSize' } ); + this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } ); + + this.$body.append( this.feedbackPanel.$element ); + }; - /** - * Display the feedback form - * @param {Object} [contents] Prefilled contents for the feedback form. - * @param {string} [contents.subject] The subject of the feedback - * @param {string} [contents.message] The content of the feedback - */ - displayForm: function ( contents ) { - var fb = this, - formButtons = {}; - - this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : ''; - this.messageInput.value = ( contents && contents.message ) ? contents.message : ''; - - this.display( 'form' ); - - // Set up buttons for dialog box. We have to do it the hard way since the json keys are localized - formButtons[ mw.msg( 'feedback-submit' ) ] = function () { - fb.submit(); - }; - formButtons[ mw.msg( 'feedback-cancel' ) ] = function () { - fb.cancel(); - }; - this.$dialog.dialog( { buttons: formButtons } ); // put the buttons back - }, + /** + * Validate the feedback form + */ + mw.Feedback.Dialog.prototype.validateFeedbackForm = function () { + var isValid = ( + ( + !this.useragentMandatory || + this.useragentCheckbox.isSelected() + ) && + ( + !!this.feedbackMessageInput.getValue() || + !!this.feedbackSubjectInput.getValue() + ) + ); + + this.actions.setAbilities( { submit: isValid } ); + }; - /** - * Display an error on the form. - * - * @param {string} message Should be a valid message key. - */ - displayError: function ( message ) { - var fb = this, - closeButton = {}; - - this.display( 'error' ); - this.$dialog.find( '.feedback-error-msg' ).msg( message ); - closeButton[ mw.msg( 'feedback-close' ) ] = function () { - fb.$dialog.dialog( 'close' ); - }; - this.$dialog.dialog( { buttons: closeButton } ); - }, + /** + * @inheritdoc + */ + mw.Feedback.Dialog.prototype.getBodyHeight = function () { + return this.feedbackPanel.$element.outerHeight( true ); + }; - /** - * Close the feedback form. - */ - cancel: function () { - this.$dialog.dialog( 'close' ); - }, + /** + * @inheritdoc + */ + mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) { + return mw.Feedback.Dialog.super.prototype.getSetupProcess.call( this, data ) + .next( function () { + var plainMsg, parsedMsg, + settings = data.settings; + data.contents = data.contents || {}; + + // Prefill subject/message + this.feedbackSubjectInput.setValue( data.contents.subject ); + this.feedbackMessageInput.setValue( data.contents.message ); + + this.status = ''; + this.messagePosterPromise = settings.messagePosterPromise; + this.setBugReportLink( settings.bugsTaskSubmissionLink ); + this.feedbackPageTitle = settings.title; + this.feedbackPageName = settings.title.getNameText(); + this.feedbackPageUrl = settings.title.getUrl(); + + // Useragent checkbox + if ( settings.useragentCheckbox.show ) { + this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message ); + } - /** - * Submit the feedback form. - */ - submit: function () { - var subject, message, - fb = this; - - // Get the values to submit. - subject = this.subjectInput.value; - - // We used to include "mw.html.escape( navigator.userAgent )" but there are legal issues - // with posting this without their explicit consent - message = this.messageInput.value; - if ( message.indexOf( '~~~' ) === -1 ) { - message += ' ~~~~'; - } + this.useragentMandatory = settings.useragentCheckbox.mandatory; + this.useragentFieldLayout.toggle( settings.useragentCheckbox.show ); + + // HACK: Setting a link in the messages doesn't work. There is already a report + // about this, and the bug report offers a somewhat hacky work around that + // includes setting a separate message to be parsed. + // We want to make sure the user can configure both the title of the page and + // a separate url, so this must be allowed to parse correctly. + // See https://phabricator.wikimedia.org/T49395#490610 + mw.messages.set( { + 'feedback-dialog-temporary-message': + '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>' + } ); + plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain(); + mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } ); + parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' ); + this.feedbackMessageLabel.setLabel( + // Double-parse + $( '<span>' ) + .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() ) + ); - this.displaySubmitting(); - - // Post the message, resolving redirects - this.api.newSection( - this.title, - subject, - message, - { redirect: true } - ) - .done( function ( result ) { - if ( result.edit !== undefined ) { - if ( result.edit.result === 'Success' ) { - fb.displayThanks(); - } else { - // unknown API result - fb.displayError( 'feedback-error1' ); - } - } else { - // edit failed - fb.displayError( 'feedback-error2' ); + this.validateFeedbackForm(); + }, this ); + }; + + /** + * @inheritdoc + */ + mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) { + return mw.Feedback.Dialog.super.prototype.getReadyProcess.call( this, data ) + .next( function () { + this.feedbackSubjectInput.focus(); + }, this ); + }; + + /** + * @inheritdoc + */ + mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) { + if ( action === 'cancel' ) { + return new OO.ui.Process( function () { + this.close( { action: action } ); + }, this ); + } else if ( action === 'external' ) { + return new OO.ui.Process( function () { + // Open in a new window + window.open( this.getBugReportLink(), '_blank' ); + // Close the dialog + this.close(); + }, this ); + } else if ( action === 'submit' ) { + return new OO.ui.Process( function () { + var fb = this, + userAgentMessage = ':' + + '<small>' + + mw.msg( 'feedback-useragent' ) + + ' ' + + mw.html.escape( navigator.userAgent ) + + '</small>\n\n', + subject = this.feedbackSubjectInput.getValue(), + message = this.feedbackMessageInput.getValue(); + + // Add user agent if checkbox is selected + if ( this.useragentCheckbox.isSelected() ) { + message = userAgentMessage + message; } - } ) - .fail( function () { - // ajax request failed - fb.displayError( 'feedback-error3' ); - } ); - }, - /** - * Modify the display form, and then open it, focusing interface on the subject. - * @param {Object} [contents] Prefilled contents for the feedback form. - * @param {string} [contents.subject] The subject of the feedback - * @param {string} [contents.message] The content of the feedback - */ - launch: function ( contents ) { - this.displayForm( contents ); - this.$dialog.dialog( 'open' ); - this.subjectInput.focus(); + // Post the message + return this.messagePosterPromise.then( function ( poster ) { + return fb.postMessage( poster, subject, message ); + }, function () { + fb.status = 'error4'; + mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' ); + } ).always( function () { + fb.close(); + } ); + }, this ); } + // Fallback to parent handler + return mw.Feedback.Dialog.super.prototype.getActionProcess.call( this, action ); }; + + /** + * Posts the message + * + * @private + * + * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback + * @param {string} subject Subject of message + * @param {string} message Body of message + * @return {jQuery.Promise} Promise representing success of message posting action + */ + mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) { + var fb = this; + + return poster.post( + subject, + message + ).then( function () { + fb.status = 'submitted'; + }, function ( mainCode, secondaryCode, details ) { + if ( mainCode === 'api-fail' ) { + if ( secondaryCode === 'http' ) { + fb.status = 'error3'; + // ajax request failed + mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus ); + } else { + fb.status = 'error2'; + mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode ); + } + } else { + fb.status = 'error1'; + } + } ); + }; + + /** + * @inheritdoc + */ + mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) { + return mw.Feedback.Dialog.super.prototype.getTeardownProcess.call( this, data ) + .first( function () { + this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl ); + // Cleanup + this.status = ''; + this.feedbackPageTitle = null; + this.feedbackSubjectInput.setValue( '' ); + this.feedbackMessageInput.setValue( '' ); + this.useragentCheckbox.setSelected( false ); + }, this ); + }; + + /** + * Set the bug report link + * @param {string} link Link to the external bug report form + */ + mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) { + this.bugReportLink = link; + }; + + /** + * Get the bug report link + * @returns {string} Link to the external bug report form + */ + mw.Feedback.Dialog.prototype.getBugReportLink = function () { + return this.bugReportLink; + }; + }( mediaWiki, jQuery ) ); |