e907a985b117a78c829b3bdc4dc6c7ebdab0bedd
[lhc/web/wiklou.git] / resources / src / mediawiki.action / mediawiki.action.edit.preview.js
1 /*!
2 * Live edit preview.
3 */
4 ( function () {
5
6 /**
7 * @ignore
8 * @param {jQuery.Event} e
9 */
10 function doLivePreview( e ) {
11 var isDiff, api, parseRequest, diffRequest, postData, copySelectors, section, summary,
12 $wikiPreview, $wikiDiff, $editform, $textbox, $copyElements, $spinner, $errorBox;
13
14 isDiff = ( e.target.name === 'wpDiff' );
15 $wikiPreview = $( '#wikiPreview' );
16 $wikiDiff = $( '#wikiDiff' );
17 $editform = $( '#editform' );
18 $textbox = $editform.find( '#wpTextbox1' );
19
20 summary = OO.ui.infuse( $( '#wpSummaryWidget' ) );
21
22 $spinner = $( '.mw-spinner-preview' );
23 $errorBox = $( '.errorbox' );
24 section = $editform.find( '[name="wpSection"]' ).val();
25
26 if ( $textbox.length === 0 ) {
27 return;
28 }
29 // Show changes for a new section is not yet supported
30 if ( isDiff && section === 'new' ) {
31 return;
32 }
33 e.preventDefault();
34
35 // Remove any previously displayed errors
36 $errorBox.remove();
37 // Show #wikiPreview if it's hidden to be able to scroll to it
38 // (if it is hidden, it's also empty, so nothing changes in the rendering)
39 $wikiPreview.show();
40
41 // Jump to where the preview will appear
42 $wikiPreview[ 0 ].scrollIntoView();
43
44 copySelectors = [
45 // Main
46 '.mw-indicators',
47 '#firstHeading',
48 '#wikiPreview',
49 '#wikiDiff',
50 '#catlinks',
51 '#p-lang',
52 // Editing-related
53 '.templatesUsed',
54 '.limitreport',
55 '.mw-summary-preview'
56 ];
57 $copyElements = $( copySelectors.join( ',' ) );
58
59 // Not shown during normal preview, to be removed if present
60 $( '.mw-newarticletext' ).remove();
61
62 if ( $spinner.length === 0 ) {
63 $spinner = $.createSpinner( {
64 size: 'large',
65 type: 'block'
66 } )
67 .addClass( 'mw-spinner-preview' )
68 .css( 'margin-top', '1em' );
69 $wikiPreview.before( $spinner );
70 } else {
71 $spinner.show();
72 }
73
74 // Can't use fadeTo because it calls show(), and we might want to keep some elements hidden
75 // (e.g. empty #catlinks)
76 // FIXME: Use CSS transition
77 // eslint-disable-next-line jquery/no-animate
78 $copyElements.animate( { opacity: 0.4 }, 'fast' );
79
80 api = new mw.Api();
81 postData = {
82 formatversion: 2,
83 action: 'parse',
84 title: mw.config.get( 'wgPageName' ),
85 summary: summary.getValue(),
86 prop: ''
87 };
88
89 if ( isDiff ) {
90 $wikiPreview.hide();
91
92 if ( postData.summary ) {
93 parseRequest = api.post( postData );
94 }
95
96 diffRequest = api.post( {
97 formatversion: 2,
98 action: 'query',
99 prop: 'revisions',
100 titles: mw.config.get( 'wgPageName' ),
101 rvdifftotext: $textbox.textSelection( 'getContents' ),
102 rvdifftotextpst: true,
103 rvprop: '',
104 rvsection: section === '' ? undefined : section,
105 uselang: mw.config.get( 'wgUserLanguage' )
106 } );
107
108 // Wait for the summary before showing the diff so the page doesn't jump twice
109 $.when( diffRequest, parseRequest ).done( function ( response ) {
110 var diffHtml;
111 try {
112 diffHtml = response[ 0 ].query.pages[ 0 ]
113 .revisions[ 0 ].diff.body;
114 $wikiDiff.find( 'table.diff tbody' ).html( diffHtml );
115 mw.hook( 'wikipage.diff' ).fire( $wikiDiff.find( 'table.diff' ) );
116 } catch ( err ) {
117 // "result.blah is undefined" error, ignore
118 mw.log.warn( err );
119 }
120 $wikiDiff.show();
121 } );
122 } else {
123 $wikiDiff.hide();
124
125 $.extend( postData, {
126 prop: 'text|indicators|displaytitle|modules|jsconfigvars|categorieshtml|templates|langlinks|limitreporthtml',
127 text: $textbox.textSelection( 'getContents' ),
128 pst: true,
129 preview: true,
130 sectionpreview: section !== '',
131 disableeditsection: true,
132 useskin: mw.config.get( 'skin' ),
133 uselang: mw.config.get( 'wgUserLanguage' )
134 } );
135 if ( section === 'new' ) {
136 postData.section = 'new';
137 postData.sectiontitle = postData.summary;
138 }
139
140 parseRequest = api.post( postData );
141 parseRequest.done( function ( response ) {
142 var newList, $displaytitle, $content, $parent, $list;
143 if ( response.parse.jsconfigvars ) {
144 mw.config.set( response.parse.jsconfigvars );
145 }
146 if ( response.parse.modules ) {
147 mw.loader.load( response.parse.modules.concat(
148 response.parse.modulescripts,
149 response.parse.modulestyles
150 ) );
151 }
152
153 newList = [];
154 // eslint-disable-next-line jquery/no-each-util
155 $.each( response.parse.indicators, function ( name, indicator ) {
156 newList.push(
157 $( '<div>' )
158 .addClass( 'mw-indicator' )
159 .attr( 'id', mw.util.escapeIdForAttribute( 'mw-indicator-' + name ) )
160 .html( indicator )
161 .get( 0 ),
162 // Add a whitespace between the <div>s because
163 // they get displayed with display: inline-block
164 document.createTextNode( '\n' )
165 );
166 } );
167 $( '.mw-indicators' ).empty().append( newList );
168
169 if ( response.parse.displaytitle ) {
170 $displaytitle = $( $.parseHTML( response.parse.displaytitle ) );
171 $( '#firstHeading' ).msg(
172 mw.config.get( 'wgEditMessage', 'editing' ),
173 $displaytitle
174 );
175 document.title = mw.msg(
176 'pagetitle',
177 mw.msg(
178 mw.config.get( 'wgEditMessage', 'editing' ),
179 $displaytitle.text()
180 )
181 );
182 }
183 if ( response.parse.categorieshtml ) {
184 $content = $( $.parseHTML( response.parse.categorieshtml ) );
185 mw.hook( 'wikipage.categories' ).fire( $content );
186 $( '.catlinks[data-mw="interface"]' ).replaceWith( $content );
187 }
188 if ( response.parse.templates ) {
189 newList = response.parse.templates.map( function ( template ) {
190 return $( '<li>' )
191 .append( $( '<a>' )
192 .attr( {
193 href: mw.util.getUrl( template.title ),
194 class: ( template.exists ? '' : 'new' )
195 } )
196 .text( template.title )
197 );
198 } );
199
200 $editform.find( '.templatesUsed .mw-editfooter-list' ).detach().empty().append( newList ).appendTo( '.templatesUsed' );
201 }
202 if ( response.parse.limitreporthtml ) {
203 $( '.limitreport' ).html( response.parse.limitreporthtml );
204 }
205 if ( response.parse.langlinks && mw.config.get( 'skin' ) === 'vector' ) {
206 newList = response.parse.langlinks.map( function ( langlink ) {
207 var bcp47 = mw.language.bcp47( langlink.lang );
208 return $( '<li>' )
209 .addClass( 'interlanguage-link interwiki-' + langlink.lang )
210 .append( $( '<a>' )
211 .attr( {
212 href: langlink.url,
213 title: langlink.title + ' - ' + langlink.langname,
214 lang: bcp47,
215 hreflang: bcp47
216 } )
217 .text( langlink.autonym )
218 );
219 } );
220 $list = $( '#p-lang ul' );
221 $parent = $list.parent();
222 $list.detach().empty().append( newList ).prependTo( $parent );
223 }
224
225 if ( response.parse.text ) {
226 $content = $wikiPreview.children( '.mw-content-ltr,.mw-content-rtl' );
227 $content
228 .detach()
229 .html( response.parse.text );
230
231 mw.hook( 'wikipage.content' ).fire( $content );
232
233 // Reattach
234 $wikiPreview.append( $content );
235
236 $wikiPreview.show();
237 }
238 } );
239 }
240 $.when( parseRequest, diffRequest ).done( function ( parseResp ) {
241 var parse = parseResp && parseResp[ 0 ].parse,
242 isSubject = ( section === 'new' ),
243 summaryMsg = isSubject ? 'subject-preview' : 'summary-preview',
244 $summaryPreview = $editform.find( '.mw-summary-preview' ).empty();
245 if ( parse && parse.parsedsummary ) {
246 $summaryPreview.append(
247 mw.message( summaryMsg ).parse(),
248 ' ',
249 $( '<span>' ).addClass( 'comment' ).html(
250 // There is no equivalent to rawParams
251 mw.message( 'parentheses' ).escaped()
252 // .replace() use $ as start of a pattern.
253 // $$ is the pattern for '$'.
254 // The inner .replace() duplicates any $ and
255 // the outer .replace() simplifies the $$.
256 .replace( '$1', parse.parsedsummary.replace( /\$/g, '$$$$' ) )
257 )
258 );
259 }
260 mw.hook( 'wikipage.editform' ).fire( $editform );
261 } ).always( function () {
262 $spinner.hide();
263 // FIXME: Use CSS transition
264 // eslint-disable-next-line jquery/no-animate
265 $copyElements.animate( {
266 opacity: 1
267 }, 'fast' );
268 } ).fail( function ( code, result ) {
269 // This just shows the error for whatever request failed first
270 var errorMsg = 'API error: ' + code;
271 if ( code === 'http' ) {
272 errorMsg = 'HTTP error: ';
273 if ( result.exception ) {
274 errorMsg += result.exception;
275 } else {
276 errorMsg += result.textStatus;
277 }
278 }
279 $errorBox = $( '<div>' )
280 .addClass( 'errorbox' )
281 .html( '<strong>' + mw.message( 'previewerrortext' ).escaped() + '</strong><br>' )
282 .append( document.createTextNode( errorMsg ) );
283 $wikiDiff.hide();
284 $wikiPreview.hide().before( $errorBox );
285 } );
286 }
287
288 $( function () {
289 // Do not enable on user .js/.css pages, as there's no sane way of "previewing"
290 // the scripts or styles without reloading the page.
291 if ( $( '#mw-userjsyoucanpreview' ).length || $( '#mw-usercssyoucanpreview' ).length ) {
292 return;
293 }
294
295 // The following elements can change in a preview but are not output
296 // by the server when they're empty until the preview response.
297 // TODO: Make the server output these always (in a hidden state), so we don't
298 // have to fish and (hopefully) put them in the right place (since skins
299 // can change where they are output).
300
301 if ( !document.getElementById( 'p-lang' ) && document.getElementById( 'p-tb' ) && mw.config.get( 'skin' ) === 'vector' ) {
302 $( '.portal:last' ).after(
303 $( '<div>' ).attr( {
304 class: 'portal',
305 id: 'p-lang',
306 role: 'navigation',
307 'aria-labelledby': 'p-lang-label'
308 } )
309 .append( $( '<h3>' ).attr( 'id', 'p-lang-label' ).text( mw.msg( 'otherlanguages' ) ) )
310 .append( $( '<div>' ).addClass( 'body' ).append( '<ul>' ) )
311 );
312 }
313
314 if ( !$( '.mw-summary-preview' ).length ) {
315 $( '#wpSummaryWidget' ).after(
316 $( '<div>' ).addClass( 'mw-summary-preview' )
317 );
318 }
319
320 if ( !document.getElementById( 'wikiDiff' ) && document.getElementById( 'wikiPreview' ) ) {
321 $( '#wikiPreview' ).after(
322 $( '<div>' )
323 .hide()
324 .attr( 'id', 'wikiDiff' )
325 .html( '<table class="diff"><col class="diff-marker"/><col class="diff-content"/>' +
326 '<col class="diff-marker"/><col class="diff-content"/><tbody/></table>' )
327 );
328 }
329
330 // This should be moved down to '#editform', but is kept on the body for now
331 // because the LiquidThreads extension is re-using this module with only half
332 // the EditPage (doesn't include #editform presumably, T57463).
333 $( document.body ).on( 'click', '#wpPreview, #wpDiff', doLivePreview );
334 } );
335
336 }() );