└── EmbeddedSurvey.js
/EmbeddedSurvey.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Create an injectable element where you want the survey to appear, and include survey configuration with
3 | * an `injectInto` selecting this element. The element will be replaced when the survey is displayed.
4 | *
5 | *
6 | * {{int:wmde-tw-template-survey-nojs-notice}}
7 | *
8 | *
9 | * Use the URL query parameter "?forcesurvey=true" to show the survey regardless of your Do Not Track setting and
10 | * any previously submitted responses.
11 | *
12 | * Responses are submitted to the QuickSurveysResponses EventLogging schema, with "answer" set to a comma-separated
13 | * list of the answer messages the user selected.
14 | *
15 | * TODO:
16 | * - Move EmbeddedSurvey into module file.
17 | * - Provide surveys in mediawiki-config or on-wiki.
18 | */
19 | ( function () {
20 | var surveyConfig = {
21 | surveys: [
22 | {
23 | injectInto: '#survey-inject-1',
24 | name: "wmde-tw-template-survey-prototype-1",
25 | question: 'wmde-tw-template-survey-prototype1-question',
26 | description: 'wmde-tw-template-survey-prototype1-description-message',
27 | answers: [
28 | 'wmde-tw-template-survey-prototype1-answer-1a',
29 | 'wmde-tw-template-survey-prototype1-answer-1b',
30 | 'wmde-tw-template-survey-prototype1-answer-1c'
31 | ]
32 | }
33 | ]
34 | };
35 |
36 | /**
37 | * Extend an object with extra properties.
38 | *
39 | * @ignore
40 | * @param {Object} target Object to extend.
41 | * @param {Object} mixin Properties to incorporate into the target.
42 | */
43 | function extend( target, mixin ) {
44 | var key;
45 | for ( key in mixin ) {
46 | target[ key ] = mixin[ key ];
47 | }
48 | }
49 |
50 | /**
51 | * Get edit count bucket name, based on the number of edits made.
52 | *
53 | * @param {number|null} editCount
54 | * @return {string}
55 | */
56 | function getEditCountBucket( editCount ) {
57 | if ( editCount >= 1000 ) {
58 | return '1000+ edits';
59 | }
60 | if ( editCount >= 100 ) {
61 | return '100-999 edits';
62 | }
63 | if ( editCount >= 5 ) {
64 | return '5-99 edits';
65 | }
66 | if ( editCount >= 1 ) {
67 | return '1-4 edits';
68 | }
69 | return '0 edits';
70 | }
71 |
72 | function getSurveyStorageKey( name ) {
73 | return 'wmde-tw-survey-' + name.replace( / /g, '-' );
74 | }
75 |
76 | function getSurveyToken( name ) {
77 | return mw.storage.get( getSurveyStorageKey( name ) );
78 | }
79 |
80 | function setSurveyToken( name, token ) {
81 | var storageId = getSurveyStorageKey( name );
82 | mw.storage.set( storageId, token );
83 | }
84 |
85 | function show() {
86 | /**
87 | * @class EmbeddedSurvey
88 | * @extends OO.ui.StackLayout
89 | *
90 | * @constructor
91 | * @param {Object} config
92 | */
93 | function EmbeddedSurvey( config ) {
94 | this.initialize( config );
95 | }
96 |
97 | OO.inheritClass( EmbeddedSurvey, OO.ui.StackLayout );
98 |
99 | extend( EmbeddedSurvey.prototype, {
100 | /**
101 | * Specifies partials (sub-templates) for use by the widget
102 | * @property {Object}
103 | */
104 | templatePartials: {
105 | // eslint-disable-next-line no-jquery/no-parse-html-literal
106 | initialPanel: $(
107 | '' +
108 | '
' +
109 | '
' +
110 | '
' +
111 | '' +
112 | '
'
113 | ),
114 | // eslint-disable-next-line no-jquery/no-parse-html-literal
115 | finalPanel: $(
116 | '' +
117 | '' +
118 | '' +
119 | '
'
120 | )
121 | },
122 | /**
123 | * A set of default options that are merged with config passed into the initialize function.
124 | * This is likely to change so currently no options are documented.
125 | * @cfg {Object} defaults Default options hash.
126 | */
127 | defaults: {
128 | templateData: {
129 | finalHeading: mw.msg( 'wmde-tw-survey-thank-you-notice' ),
130 | footer: mw.message( 'ext-quicksurveys-survey-privacy-policy-default-text' ).parse()
131 | },
132 | PanelLayout: {
133 | expanded: false,
134 | framed: false,
135 | padded: true,
136 | classes: [ 'message content' ]
137 | },
138 | scrollable: false,
139 | expanded: false,
140 | classes: [ 'panel panel-inline visible ext-quick-survey-panel' ]
141 | },
142 | /**
143 | * Initialise a widget.
144 | *
145 | * @param {Object} config
146 | */
147 | initialize: function ( config ) {
148 | var event;
149 |
150 | this.config = config || {};
151 | $.extend( true, this.config, this.defaults );
152 |
153 | if ( config.survey.privacyPolicy ) {
154 | // eslint-disable-next-line mediawiki/msg-doc
155 | this.config.templateData.footer = mw.message( config.survey.privacyPolicy ).parse();
156 | }
157 |
158 | // setup initial panel
159 | this.initialPanel = this.widget( 'PanelLayout', 'initialPanel' );
160 |
161 | // setup final panel
162 | this.finalPanel = this.widget( 'PanelLayout', 'finalPanel' );
163 |
164 | // Set the buttons
165 | this.renderButtons();
166 |
167 | // setup stack
168 | EmbeddedSurvey.super.call( this, $.extend( {}, config, {
169 | items: [ this.initialPanel, this.finalPanel ]
170 | } ) );
171 |
172 | event = {
173 | beaconCapable: !!navigator.sendBeacon,
174 | surveySessionToken: this.config.surveySessionToken,
175 | pageviewToken: this.config.pageviewToken,
176 | surveyCodeName: this.config.survey.name,
177 | eventName: 'eligible'
178 | };
179 |
180 | if ( window.performance && performance.now ) {
181 | event.performanceNow = Math.round( performance.now() );
182 | }
183 |
184 | mw.eventLog.logEvent( 'QuickSurveyInitiation', event );
185 | },
186 | /**
187 | * Render and append buttons (and a freeform input if set) to
188 | * the initial panel
189 | */
190 | renderButtons: function () {
191 | var $btnContainer = this.initialPanel.$element.find( '.survey-button-container' ),
192 | answers = this.config.survey.answers,
193 | answerCheckboxes,
194 | answerOptions,
195 | submitButton;
196 |
197 | answerOptions = answers.map( function ( answer ) {
198 | return {
199 | data: answer,
200 | label: mw.msg( answer )
201 | };
202 | } );
203 | answerCheckboxes = new OO.ui.CheckboxMultiselectInputWidget( {
204 | // eslint-disable-next-line mediawiki/msg-doc
205 | options: answerOptions
206 | } );
207 |
208 | answerCheckboxes.$element.appendTo( $btnContainer );
209 |
210 | submitButton = new OO.ui.ButtonWidget( {
211 | label: mw.msg( 'ext-quicksurveys-internal-freeform-survey-submit-button' ),
212 | flags: 'progressive'
213 | } );
214 | submitButton.$element.appendTo( $btnContainer );
215 |
216 | submitButton.connect( this, {
217 | click: [ 'onClickSubmitButton', answerCheckboxes ]
218 | } );
219 | },
220 | /**
221 | * Make a brand spanking new OOUI widget from a template partial
222 | *
223 | * @param {string} widgetName a valid OOUI widget
224 | * @param {string} [templatePartialName] name of a registered template partial
225 | * @param {Object} [options] further options to be passed to the widget
226 | * @return {*} OOUI widget instance
227 | */
228 | widget: function ( widgetName, templatePartialName, options ) {
229 | var templateClone,
230 | template = this.templatePartials[ templatePartialName ],
231 | config = $.extend( {}, this.config[ widgetName ], options ),
232 | templateData = this.config.templateData;
233 |
234 | if ( template ) {
235 | templateClone = template.clone();
236 | templateClone.find( '[data-question]' ).text( templateData.question );
237 | templateClone.find( '[data-description]' ).text( templateData.description );
238 | templateClone.find( '[data-footer]' ).html( templateData.footer );
239 | templateClone.find( '[data-finalHeading]' ).text( templateData.finalHeading );
240 | config.$content = templateClone;
241 | }
242 |
243 | return new OO.ui[ widgetName ]( config );
244 | },
245 | /**
246 | * Log the answer to Schema:QuickSurveysResponses
247 | * See {@link https://meta.wikimedia.org/wiki/Schema:QuickSurveysResponses}
248 | *
249 | * @param {string} answer
250 | * @return {jQuery.Deferred}
251 | */
252 | log: function ( answer ) {
253 | var survey = this.config.survey,
254 | skin = mw.config.get( 'skin' ),
255 | // FIXME: remove this when SkinMinervaBeta is renamed to 'minerva-beta'.
256 | mobileMode = mw.config.get( 'wgMFMode' );
257 |
258 | // On mobile differentiate between minerva stable and beta
259 | // by appending 'beta' to 'minerva'
260 | if ( skin === 'minerva' && mobileMode === 'beta' ) {
261 | skin += mobileMode;
262 | }
263 |
264 | return mw.eventLog.logEvent( 'QuickSurveysResponses', {
265 | namespaceId: mw.config.get( 'wgNamespaceNumber' ),
266 | surveySessionToken: this.config.surveySessionToken,
267 | pageviewToken: this.config.pageviewToken,
268 | pageId: mw.config.get( 'wgArticleId' ),
269 | pageTitle: mw.config.get( 'wgPageName' ),
270 | surveyCodeName: survey.name,
271 | surveyResponseValue: answer,
272 | platform: 'web',
273 | skin: skin,
274 | isTablet: !this.config.isMobileLayout,
275 | userLanguage: mw.config.get( 'wgContentLanguage' ),
276 | isLoggedIn: !mw.user.isAnon(),
277 | editCountBucket: getEditCountBucket( mw.config.get( 'wgUserEditCount' ) ),
278 | countryCode: 'Unknown'
279 | } );
280 | },
281 |
282 | /**
283 | * @param {OO.ui.CheckboxMultiselectInputWidget} checkboxes
284 | * @private
285 | */
286 | onClickSubmitButton: function ( checkboxes ) {
287 | var selections = checkboxes.getValue();
288 |
289 | this.submit( selections.join(',') );
290 | },
291 |
292 | /**
293 | * Submit user's answer to the backend and show the next panel
294 | *
295 | * @param {string} answer
296 | */
297 | submit: function ( answer ) {
298 | this.log( answer );
299 | /**
300 | * @event dismiss fired when any of the buttons in the survey are selected.
301 | */
302 | this.emit( 'dismiss' );
303 | this.setItem( this.finalPanel );
304 | }
305 | } );
306 |
307 | for ( var survey of surveyConfig.surveys ) {
308 | var root = $( survey.injectInto ),
309 | isMobileLayout = window.innerWidth <= 768,
310 | userToken = getSurveyToken( survey.name );
311 |
312 | if ( !root.length ) {
313 | continue;
314 | }
315 |
316 | if ( userToken === '~' &&
317 | !mw.util.getParamValue( 'forcesurvey' )
318 | ) {
319 | root.html( mw.msg( 'wmde-tw-survey-thank-you-notice' ) );
320 | continue;
321 | }
322 |
323 | if ( !userToken ) {
324 | // Generate a new token for each survey
325 | userToken = mw.user.generateRandomSessionId();
326 | setSurveyToken( survey.name, userToken );
327 | }
328 |
329 | var panel = new EmbeddedSurvey( {
330 | survey: survey,
331 | templateData: {
332 | // eslint-disable-next-line mediawiki/msg-doc
333 | question: mw.msg( survey.question ),
334 | // eslint-disable-next-line mediawiki/msg-doc
335 | description: survey.description ? mw.msg( survey.description ) : ''
336 | },
337 | surveySessionToken: mw.user.sessionId() + '-quicksurveys',
338 | pageviewToken: mw.user.getPageviewToken(),
339 | isMobileLayout: isMobileLayout
340 | } );
341 |
342 | panel.on( 'dismiss', function () {
343 | setSurveyToken( survey.name, '~' );
344 | } );
345 | // TODO: Move inline CSS to an external stylesheet.
346 | root.replaceWith( panel.$element.css( 'float', 'none' ) );
347 | }
348 | }
349 |
350 | function shouldDisplay() {
351 | if ( mw.util.getParamValue( 'forcesurvey' ) ) {
352 | return true;
353 | }
354 |
355 | if (
356 | /1|yes/.test( navigator.doNotTrack ) ||
357 | window.doNotTrack === '1'
358 | ) {
359 | return 'wmde-tw-template-survey-dnt-notice';
360 | }
361 |
362 | return true;
363 | }
364 |
365 | function init() {
366 | mw.loader.using(
367 | [
368 | 'ext.quicksurveys.lib',
369 | 'mediawiki.cookie',
370 | 'mediawiki.storage',
371 | 'mediawiki.viewport',
372 | 'mediawiki.experiments',
373 | 'oojs-ui-core',
374 | 'oojs-ui-widgets',
375 | 'mediawiki.user',
376 | 'mediawiki.Uri',
377 | 'ext.eventLogging',
378 | 'mediawiki.util'
379 | ],
380 | show
381 | );
382 | }
383 |
384 | function showNotice( message ) {
385 | $( '.wmde-tw-template-survey-container' )
386 | .html( mw.msg( message ) );
387 | }
388 |
389 | $( function () {
390 | var isDisplayed = shouldDisplay();
391 |
392 | if ( isDisplayed === true ) {
393 | init();
394 | } else {
395 | showNotice( isDisplayed );
396 | }
397 | // TODO: else, show placeholder and optional reason.
398 | } );
399 | } )();
400 |
--------------------------------------------------------------------------------