2 const api = new mw.Api();
3 const util = require( 'mediawiki.util' );
6 * Show the edit summary.
9 * @param {jQuery} $formNode
10 * @param {Object} response
12 function showEditSummary( $formNode, response ) {
13 const $summaryPreview = $formNode.find( '.mw-summary-preview' ).empty();
14 const parse = response.parse;
16 if ( !parse || !parse.parsedsummary ) {
20 $summaryPreview.append(
21 mw.message( 'summary-preview' ).parse(),
23 $( '<span>' ).addClass( 'comment' ).html( parenthesesWrap( parse.parsedsummary ) )
28 * Wrap a string in parentheses.
34 function parenthesesWrap( str ) {
38 // There is no equivalent to rawParams
39 return mw.message( 'parentheses' ).escaped()
40 // Specify a function as the replacement,
41 // so that "$" characters in str are not interpreted.
42 .replace( '$1', () => str );
46 * Show status indicators.
49 * @param {Array} indicators
51 function showIndicators( indicators ) {
52 // eslint-disable-next-line no-jquery/no-map-util
53 indicators = $.map( indicators, ( indicator, name ) => $( '<div>' )
54 .addClass( 'mw-indicator' )
55 .attr( 'id', mw.util.escapeIdForAttribute( 'mw-indicator-' + name ) )
58 if ( indicators.length ) {
59 mw.hook( 'wikipage.indicators' ).fire( $( indicators ) );
62 // Add whitespace between the <div>s because
63 // they get displayed with display: inline-block
65 indicators.forEach( ( indicator ) => {
66 newList.push( indicator, document.createTextNode( '\n' ) );
69 $( '.mw-indicators' ).empty().append( newList );
73 * Show the templates used.
75 * The formatting here repeats what is done in includes/TemplatesOnThisPageFormatter.php
78 * @param {Array} templates List of template titles.
80 function showTemplates( templates ) {
81 // The .templatesUsed div can be empty, if no templates are in use.
82 // In that case, we have to create the required structure.
83 const $parent = $( '.templatesUsed' );
85 // Find or add the explanation text (the toggler for collapsing).
86 let $explanation = $parent.find( '.mw-templatesUsedExplanation p' );
87 if ( $explanation.length === 0 ) {
88 $explanation = $( '<p>' );
89 $parent.append( $( '<div>' )
90 .addClass( 'mw-templatesUsedExplanation' )
91 .append( $explanation ) );
94 // Find or add the list. The makeCollapsible() method is called on this
95 // in resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
96 let $list = $parent.find( 'ul' );
97 if ( $list.length === 0 ) {
98 $list = $( '<ul>' ).addClass( [ 'mw-editfooter-list', 'mw-collapsible', 'mw-made-collapsible' ] );
99 $parent.append( $list );
102 if ( templates.length === 0 ) {
103 $explanation.msg( 'templatesusedpreview', 0 );
108 // Fetch info about all templates, batched because API is limited to 50 at a time.
109 $parent.addClass( 'mw-preview-loading-elements-loading' );
110 const batchSize = 50;
112 for ( let batch = 0; batch < templates.length; batch += batchSize ) {
113 // Build a list of template names for this batch.
114 const titles = templates
115 .slice( batch, batch + batchSize )
116 .map( ( template ) => template.title );
117 requests.push( api.post( {
123 // @todo Do we need inlinkcontext here?
124 inprop: 'linkclasses|protection',
125 intestactions: 'edit'
128 $.when( ...requests ).done( function () {
129 const templatesAllInfo = [];
130 // For the first batch, empty the list in preparation for either adding new items or not needing to.
131 for ( let r = 0; r < arguments.length; r++ ) {
132 // Response is either the whole argument, or the 0th element of it.
133 const response = arguments[ r ][ 0 ] || arguments[ r ];
134 const templatesInfo = ( response.query && response.query.pages ) || [];
135 templatesInfo.forEach( ( ti ) => {
136 templatesAllInfo.push( {
137 title: mw.Title.newFromText( ti.title ),
142 // Sort alphabetically.
143 templatesAllInfo.sort( ( t1, t2 ) => {
144 // Compare titles with the same rules of Title::compare() in PHP.
145 if ( t1.title.getNamespaceId() !== t2.title.getNamespaceId() ) {
146 return t1.title.getNamespaceId() - t2.title.getNamespaceId();
148 return t1.title.getMain() === t2.title.getMain() ?
150 t1.title.getMain() < t2.title.getMain() ? -1 : 1;
154 // Add new template list, and update the list header.
155 const $listNew = $( '<ul>' );
156 addItemToTemplateListPromise( $listNew, templatesAllInfo, 0 )
158 $list.html( $listNew.html() );
160 $explanation.msg( 'templatesusedpreview', templatesAllInfo.length );
162 $parent.removeClass( 'mw-preview-loading-elements-loading' );
167 * Recursive function to add a template link to the list of templates in use.
168 * This is useful because addItemToTemplateList() might need to make extra API requests to fetch
169 * messages, but we don't want to send parallel requests for these (because they're often the
170 * for the same messages).
173 * @param {jQuery} $list The `<ul>` to add the item to.
174 * @param {Object} templatesInfo All templates' info, sorted by namespace and title.
175 * @param {number} templateIndex The current item in templatesInfo (0-indexed).
176 * @return {jQuery.Promise}
178 function addItemToTemplateListPromise( $list, templatesInfo, templateIndex ) {
179 return addItemToTemplateList( $list, templatesInfo[ templateIndex ] ).then( () => {
180 if ( templatesInfo[ templateIndex + 1 ] !== undefined ) {
181 return addItemToTemplateListPromise( $list, templatesInfo, templateIndex + 1 );
187 * Create list item with relevant links for the given template, and add it to the $list.
190 * @param {jQuery} $list The `<ul>` to add the item to.
191 * @param {Object} template Template info with which to construct the `<li>`.
192 * @return {jQuery.Promise}
194 function addItemToTemplateList( $list, template ) {
195 const editable = template.apiData.ns >= 0;
196 const canEdit = editable && template.apiData.actions.edit !== undefined;
197 const linkClasses = template.apiData.linkclasses || [];
198 if ( template.apiData.missing !== undefined && template.apiData.known === undefined ) {
199 linkClasses.push( 'new' );
201 const $baseLink = $( '<a>' )
202 // Additional CSS classes (e.g. link colors) used for links to this template.
203 // The following classes might be used here:
206 // * any added by the GetLinkColours hook
207 .addClass( linkClasses );
208 const $link = $baseLink.clone()
209 .attr( 'href', template.title.getUrl() )
210 .text( template.title.getPrefixedText() );
213 const $editLink = $baseLink.clone()
214 .attr( 'href', template.title.getUrl( { action: 'edit' } ) )
215 .append( mw.msg( canEdit ? 'editlink' : 'viewsourcelink' ) );
217 const wordSep = mw.message( 'word-separator' ).escaped();
218 return getRestrictionsText( template.apiData.protection || [] )
219 .then( ( restrictionsList ) => {
220 // restrictionsList is a comma-separated parentheses-wrapped localized list of restriction level names.
221 const editLinkParens = parenthesesWrap( $editLink[ 0 ].outerHTML );
222 const $li = $( '<li>' ).append( $link, wordSep, editLinkParens, wordSep, restrictionsList );
226 $list.append( $( '<li>' ).append( $link ) );
227 return $.Deferred().resolve( '' );
232 * Get a localized string listing the restriction levels for a template.
234 * This should match the logic from TemplatesOnThisPageFormatter::getRestrictionsText().
237 * @param {Array} restrictions Set of protection info objects from the inprop=protection API.
238 * @return {jQuery.Promise}
240 function getRestrictionsText( restrictions ) {
242 if ( !restrictions ) {
243 return $.Deferred().resolve( msg );
246 // Record other restriction levels, in case it's protected for others.
247 const restrictionLevels = [];
248 restrictions.forEach( ( r ) => {
249 if ( r.type !== 'edit' ) {
252 if ( r.level === 'sysop' ) {
253 msg = mw.msg( 'template-protected' );
254 } else if ( r.level === 'autoconfirmed' ) {
255 msg = mw.msg( 'template-semiprotected' );
257 restrictionLevels.push( r.level );
261 // If sysop or autoconfirmed, use that.
263 return $.Deferred().resolve( msg );
266 // Otherwise, if the edit restriction isn't one of the backwards-compatible ones,
267 // use the (possibly custom) restriction-level-* messages.
269 restrictionLevels.forEach( ( level ) => {
270 msgs.push( 'restriction-level-' + level );
272 if ( msgs.length === 0 ) {
273 return $.Deferred().resolve( '' );
276 // Custom restriction levels don't have their messages loaded, so we have to do that.
277 return api.loadMessagesIfMissing( msgs ).then( () => {
278 const localizedMessages = msgs.map(
279 // Messages that can be used here include:
280 // * restriction-level-sysop
281 // * restriction-level-autoconfirmed
282 ( m ) => mw.message( m ).parse()
284 // There's no commaList in JS, so just join with commas (doesn't handle the last item).
285 return parenthesesWrap( localizedMessages.join( mw.msg( 'comma-separator' ) ) );
290 * Show the language links (Vector-specific).
291 * TODO: Doesn't work in vector-2022 (maybe it doesn't need to?)
294 * @param {Array} langLinks
296 function showLanguageLinks( langLinks ) {
297 const newList = langLinks.map( ( langLink ) => {
298 const bcp47 = mw.language.bcp47( langLink.lang );
299 // eslint-disable-next-line mediawiki/class-doc
301 .addClass( 'interlanguage-link interwiki-' + langLink.lang )
305 title: langLink.title + ' - ' + langLink.langname,
309 .text( langLink.autonym )
312 const $list = $( '#p-lang ul' ),
313 $parent = $list.parent();
314 $list.detach().empty().append( newList ).prependTo( $parent );
318 * Parse preview response and show a warning at the top of the preview.
321 * @param {Object} config
322 * @param {Object} response
324 function showPreviewNotes( config, response ) {
325 const arrow = $( document.body ).css( 'direction' ) === 'rtl' ? '←' : '→';
326 const $previewHeader = $( '<div>' )
327 .addClass( 'previewnote' )
329 .attr( 'id', 'mw-previewheader' )
330 // TemplateSandbox will insert an HTML string here.
331 .append( config.previewHeader )
334 const warningContentElement = $( '<div>' )
336 // TemplateSandbox will insert a jQuery here.
340 .addClass( 'mw-continue-editing' )
342 .attr( 'href', '#' + config.$formNode.attr( 'id' ) )
343 .text( arrow + ' ' + mw.msg( 'continue-editing' ) )
345 response.parse.parsewarningshtml.map( ( warning ) => $( '<p>' ).append( warning ) )
347 const warningMessageElement = util.messageBox(
348 warningContentElement,
351 $previewHeader.append( warningMessageElement );
352 config.$previewNode.prepend( $previewHeader );
356 * Show an error message in place of a preview.
359 * @param {Object} config
360 * @param {jQuery} $message
362 function showError( config, $message ) {
363 const errorContentElement = $( '<div>' )
365 $( '<strong>' ).text( mw.msg( 'previewerrortext' ) ),
368 const errorMessageElement = util.messageBox( errorContentElement, 'error' );
369 errorMessageElement.classList.add( 'mw-page-preview-error' );
370 config.$previewNode.hide().before( errorMessageElement );
371 if ( config.$diffNode ) {
372 config.$diffNode.hide();
377 * Update the various bits of the page based on the response.
380 * @param {Object} config
381 * @param {Object} response
383 function handleParseResponse( config, response ) {
386 // Js config variables and modules.
387 if ( response.parse.jsconfigvars ) {
388 mw.config.set( response.parse.jsconfigvars );
390 if ( response.parse.modules ) {
391 mw.loader.load( response.parse.modules.concat(
392 response.parse.modulestyles
397 showIndicators( response.parse.indicators );
400 if ( response.parse.displaytitle ) {
401 $( '#firstHeadingTitle' ).html( response.parse.displaytitle );
405 if ( response.parse.categorieshtml ) {
406 $content = $( $.parseHTML( response.parse.categorieshtml ) );
407 mw.hook( 'wikipage.categories' ).fire( $content );
408 $( '.catlinks[data-mw="interface"]' ).replaceWith( $content );
411 // Table of contents.
412 if ( response.parse.sections ) {
414 * Fired when dynamic changes have been made to the table of contents.
416 * @event ~'wikipage.tableOfContents'
418 * @param {Object[]} sections Metadata about each section, as returned by
419 * [API:Parse]{@link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext}.
421 mw.hook( 'wikipage.tableOfContents' ).fire(
422 response.parse.hidetoc ? [] : response.parse.sections
427 if ( response.parse.templates ) {
428 showTemplates( response.parse.templates );
432 if ( response.parse.limitreporthtml ) {
433 $( '.limitreport' ).html( response.parse.limitreporthtml )
434 .find( '.mw-collapsible' ).makeCollapsible();
438 if ( response.parse.langlinks && mw.config.get( 'skin' ) === 'vector' ) {
439 showLanguageLinks( response.parse.langlinks );
442 if ( !response.parse.text ) {
446 // Remove any previous preview
447 config.$previewNode.children( '.mw-parser-output' ).remove();
448 // Remove preview note, if present (added by Live Preview, etc.).
449 config.$previewNode.find( '.previewnote' ).remove();
451 if ( config.isLivePreview ) {
452 showPreviewNotes( config, response );
455 $content = $( $.parseHTML( response.parse.text ) );
457 config.$previewNode.append( $content ).show();
459 mw.hook( 'wikipage.content' ).fire( $content );
463 * Get the unresolved promise of the preview request.
466 * @param {Object} config
467 * @param {string|number} section
468 * @return {jQuery.Promise}
470 function getParseRequest( config, section ) {
474 summary: config.summary,
477 params[ config.titleParam ] = config.title;
479 if ( !config.showDiff ) {
480 params[ config.textParam ] = config.$textareaNode.textSelection( 'getContents' );
481 Object.assign( params, {
482 prop: 'text|indicators|displaytitle|modules|jsconfigvars|categorieshtml|sections|templates|langlinks|limitreporthtml|parsewarningshtml',
485 sectionpreview: section !== '',
486 disableeditsection: true,
487 useskin: mw.config.get( 'skin' ),
488 uselang: mw.config.get( 'wgUserLanguage' )
490 if ( mw.config.get( 'wgUserVariant' ) ) {
491 params.variant = mw.config.get( 'wgUserVariant' );
494 if ( section === 'new' ) {
495 params.section = 'new';
496 params.sectiontitle = params.summary;
497 delete params.summary;
500 Object.assign( params, config.parseParams );
502 return api.post( params, { headers: { 'Promise-Non-Write-API-Action': 'true' } } );
506 * Get the required <table> structure for displaying diffs.
510 function getDiffTable() {
511 return $( '<table>' ).addClass( 'diff' ).append(
512 $( '<col>' ).addClass( 'diff-marker' ),
513 $( '<col>' ).addClass( 'diff-content' ),
514 $( '<col>' ).addClass( 'diff-marker' ),
515 $( '<col>' ).addClass( 'diff-content' ),
516 $( '<thead>' ).append(
517 $( '<tr>' ).addClass( 'diff-title' ).append(
519 .attr( 'colspan', 2 )
520 .addClass( 'diff-otitle diff-side-deleted' )
521 .text( mw.msg( 'currentrev' ) ),
523 .attr( 'colspan', 2 )
524 .addClass( 'diff-ntitle diff-side-added' )
525 .text( mw.msg( 'yourtext' ) )
533 * Show the diff from the response.
536 * @param {Object} config
537 * @param {Object} response
539 function handleDiffResponse( config, response ) {
540 const $table = getDiffTable();
546 const diff = response.compare.bodies;
548 $table.find( 'tbody' ).html( diff.main );
549 mw.hook( 'wikipage.diff' ).fire( $table );
551 // The diff is empty.
552 const $tableCell = $( '<td>' )
553 .attr( 'colspan', 4 )
554 .addClass( 'diff-notice' )
557 .addClass( 'mw-diff-empty' )
558 .text( mw.msg( 'diff-empty' ) )
560 $table.find( 'tbody' )
563 $( '<tr>' ).append( $tableCell )
566 config.$diffNode.show();
570 * Get the unresolved promise of the diff request.
573 * @param {Object} config
574 * @param {string|number} section
575 * @param {boolean} pageExists
576 * @return {jQuery.Promise}
578 function getDiffRequest( config, section, pageExists ) {
579 let contents = config.$textareaNode.textSelection( 'getContents' ),
580 sectionTitle = config.summary;
582 if ( section === 'new' ) {
583 // T293930: Hack to show live diff for new section creation.
585 // We concatenate the section heading with the edit box text and pass it to
586 // the diff API as the full input text. This is roughly what the server-side
587 // does when difference is requested for section edit.
588 // The heading is always prepended, we do not bother with editing old rev
589 // at this point (`?action=edit&oldid=xxx§ion=new`) -- which will require
590 // mid-text insertion of the section -- because creation of new section is only
591 // possible on latest revision.
593 // The section heading text is unconditionally wrapped in <h2> heading and
594 // ends with double newlines, except when it's empty. This is for parity with the
595 // server-side rendering of the same case.
596 sectionTitle = sectionTitle === '' ? '' : '== ' + sectionTitle + ' ==\n\n';
598 // Prepend section heading to section text.
599 contents = sectionTitle + contents;
604 fromtitle: config.title,
605 totitle: config.title,
607 // Remove trailing whitespace for consistency with EditPage diffs.
608 // TODO trimEnd() when we can use that.
609 'totext-main': contents.replace( /\s+$/, '' ),
610 'tocontentmodel-main': mw.config.get( 'wgPageContentModel' ),
613 uselang: mw.config.get( 'wgUserLanguage' )
615 if ( mw.config.get( 'wgUserVariant' ) ) {
616 params.variant = mw.config.get( 'wgUserVariant' );
619 params[ 'tosection-main' ] = section;
622 params.fromslots = 'main';
623 params[ 'fromcontentmodel-main' ] = mw.config.get( 'wgPageContentModel' );
624 params[ 'fromtext-main' ] = '';
626 return api.post( params );
630 * Get the selectors of elements that should be grayed out while the preview is being generated.
632 * @memberof module:mediawiki.page.preview
636 function getLoadingSelectors() {
648 '.mw-summary-preview',
654 * Fetch and display a preview of the current editing area.
656 * @memberof module:mediawiki.page.preview
657 * @param {Object} config Configuration options.
658 * @param {jQuery} [config.$previewNode=$( '#wikiPreview' )] Where the preview should be displayed.
659 * @param {jQuery} [config.$diffNode=$( '#wikiDiff' )] Where diffs should be displayed (if showDiff is set).
660 * @param {jQuery} [config.$formNode=$( '#editform' )] The form node.
661 * @param {jQuery} [config.$textareaNode=$( '#wpTextbox1' )] The edit form's textarea.
662 * @param {jQuery} [config.$spinnerNode=$( '.mw-spinner-preview' )] The loading indicator. This will
663 * be shown/hidden accordingly while waiting for the XMLHttpRequest to complete.
664 * Ignored if it doesn't exist in the document and `createSpinner` is false.
665 * @param {string} [config.summary=null] The edit summary. If no value is given, the summary is
666 * fetched from `$( '#wpSummaryWidget' )`.
667 * @param {boolean} [config.showDiff=false] Shows a diff in the preview area instead of the content.
668 * @param {boolean} [config.isLivePreview=false] Instructs the module to replicate the
669 * server-side preview as much as possible. Specifically:
670 * - Before initiating the preview, some alerts and error messages at the top of the page will
671 * be removed, and the browser will scroll to the preview.
672 * - After finishing the preview, a reminder that it's only a preview, or an error message in
673 * case a request has failed, will be shown at the top of the preview.
674 * @param {Node|Node[]|jQuery|string} [config.previewHeader=null] Content of `<h2>` element at
675 * the top of the preview notes. Required if `isLivePreview` is true.
676 * @param {Node|Node[]|jQuery|string} [config.previewNote=null] Main text of the first preview
677 * note. Required if `isLivePreview` is true.
678 * @param {string} [config.title=mw.config.get( 'wgPageName' )] The title of the page being previewed.
679 * @param {string} [config.titleParam='title'] Name of the parse API parameter to pass `title` to.
680 * @param {string} [config.textParam='text'] Name of the parse API parameter to pass the content
681 * of `$textareaNode` to. Ignored if `showDiff` is true.
682 * @param {Object} [config.parseParams=null] Additional parse API parameters. This can override
683 * any parameter set by the module.
684 * @param {module:mediawiki.page.preview~responseHandler} [config.responseHandler=null] Callback
685 * to run right after the API responses are received. This allows the config and response
686 * objects to be modified before the preview is shown.
687 * @param {boolean} [config.createSpinner=false] Creates `$spinnerNode` and inserts it before
688 * `$previewNode` if one doesn't already exist and the module `jquery.spinner` is loaded.
689 * @param {string[]} [config.loadingSelectors=getLoadingSelectors()] An array of query selectors
690 * (i.e. '#catlinks') that should be grayed out while the preview is being generated.
691 * @return {jQuery.Promise|undefined} jQuery.Promise or `undefined` if no `$textareaNode` was provided in the config.
692 * @fires Hooks~'wikipage.categories'
693 * @fires Hooks~'wikipage.content'
694 * @fires Hooks~'wikipage.diff'
695 * @fires Hooks~'wikipage.indicators'
696 * @fires Hooks~'wikipage.tableOfContents'
699 function doPreview( config ) {
700 config = Object.assign( {
701 $previewNode: $( '#wikiPreview' ),
702 $diffNode: $( '#wikiDiff' ),
703 $formNode: $( '#editform' ),
704 $textareaNode: $( '#wpTextbox1' ),
705 $spinnerNode: $( '.mw-spinner-preview' ),
708 isLivePreview: false,
711 title: mw.config.get( 'wgPageName' ),
715 responseHandler: null,
716 createSpinner: false,
717 loadingSelectors: getLoadingSelectors()
720 const section = config.$formNode.find( '[name="wpSection"]' ).val();
722 if ( !config.$textareaNode || config.$textareaNode.length === 0 ) {
726 // Fetch edit summary, if not already given.
727 if ( !config.summary ) {
728 const $summaryWidget = $( '#wpSummaryWidget' );
729 if ( $summaryWidget.length ) {
730 config.summary = OO.ui.infuse( $summaryWidget ).getValue();
734 if ( config.isLivePreview ) {
735 // Not shown during normal preview, to be removed if present
736 $( '.mw-newarticletext, .mw-page-preview-error' ).remove();
738 // Show #wikiPreview if it's hidden to be able to scroll to it.
739 // (If it is hidden, it's also empty, so nothing changes in the rendering.)
740 config.$previewNode.show();
742 // Jump to where the preview will appear
743 config.$previewNode[ 0 ].scrollIntoView();
746 // Show or create the spinner if possible.
747 if ( config.$spinnerNode && config.$spinnerNode.length ) {
748 config.$spinnerNode.show();
749 } else if ( config.createSpinner ) {
750 if ( mw.loader.getState( 'jquery.spinner' ) === 'ready' ) {
751 config.$spinnerNode = $.createSpinner( {
755 .addClass( 'mw-spinner-preview' )
756 .insertBefore( config.$previewNode );
758 mw.log.warn( 'createSpinner requires the module jquery.spinner' );
762 // Gray out the 'copy elements' while we wait for a response.
763 const $loadingElements = $( config.loadingSelectors.join( ',' ) );
764 $loadingElements.addClass( [ 'mw-preview-loading-elements', 'mw-preview-loading-elements-loading' ] );
766 // Acquire a temporary user username before previewing or diffing, so that signatures and
767 // user-related magic words display the temp user instead of IP user in the preview. (T331397)
768 const tempUserNamePromise = mw.user.acquireTempUserName();
772 const parseRequest = tempUserNamePromise.then( () => getParseRequest( config, section ) );
774 if ( config.showDiff ) {
775 config.$previewNode.hide();
777 // Add the diff node if it doesn't exist (directly after the preview node).
778 if ( config.$diffNode.length === 0 && config.$previewNode.length > 0 ) {
779 const rtlDir = $( '#wpTextbox1' ).attr( 'dir' ) === 'rtl';
780 const alignStart = rtlDir ? 'right' : 'left';
781 config.$diffNode = $( '<div>' )
782 .attr( 'id', 'wikiDiff' )
783 // The following classes are used here:
784 // * diff-editfont-monospace
785 // * diff-editfont-sans-serif
786 // * diff-editfont-serif
787 .addClass( 'diff-editfont-' + mw.user.options.get( 'editfont' ) )
788 // The following classes are used here:
789 // * diff-contentalign-left
790 // * diff-contentalign-right
791 .addClass( 'diff-contentalign-' + alignStart );
792 config.$previewNode.after( config.$diffNode );
795 // Hide the table of contents, in case it was previously shown after previewing.
796 mw.hook( 'wikipage.tableOfContents' ).fire( [] );
798 // The compare API returns an error if the title doesn't exist and fromtext is not
799 // specified. So we have to account for the possibility that the page was created or
800 // deleted after the user started editing. Luckily the parse API returns pageid so we
801 // can wait for that.
802 // TODO: Show "Warning: This page was deleted after you started editing!"?
803 diffRequest = parseRequest.then( ( parseResponse ) => getDiffRequest( config, section, parseResponse.parse.pageid !== 0 ) );
805 } else if ( config.$diffNode ) {
806 config.$diffNode.hide();
809 return $.when( parseRequest, diffRequest )
810 .done( ( parseResponse, diffResponse ) => {
811 if ( config.responseHandler ) {
813 * @callback module:mediawiki.page.preview~responseHandler
814 * @param {Object} config Options for live preview API
815 * @param {Object} parseResponse Parse API response
816 * @param {Object} [diffResponse] Compare API response
818 if ( config.showDiff ) {
819 config.responseHandler( config, parseResponse[ 0 ], diffResponse[ 0 ] );
821 config.responseHandler( config, parseResponse[ 0 ] );
825 showEditSummary( config.$formNode, parseResponse[ 0 ] );
827 if ( config.showDiff ) {
828 handleDiffResponse( config, diffResponse[ 0 ] );
830 handleParseResponse( config, parseResponse[ 0 ] );
833 mw.hook( 'wikipage.editform' ).fire( config.$formNode );
835 .fail( ( _code, result ) => {
836 if ( config.isLivePreview ) {
837 // This just shows the error for whatever request failed first
838 showError( config, api.getErrorMessage( result ) );
842 if ( config.$spinnerNode && config.$spinnerNode.length ) {
843 config.$spinnerNode.hide();
845 $loadingElements.removeClass( 'mw-preview-loading-elements-loading' );
850 * Fetch and display a preview of the current editing area.
853 * var preview = require( 'mediawiki.page.preview' );
854 * preview.doPreview();
856 * @exports mediawiki.page.preview
859 doPreview: doPreview,
860 getLoadingSelectors: getLoadingSelectors