( function () {
const api = new mw.Api();
const util = require( 'mediawiki.util' );
/**
* Show the edit summary.
*
* @private
* @param {jQuery} $formNode
* @param {Object} response
*/
function showEditSummary( $formNode, response ) {
const $summaryPreview = $formNode.find( '.mw-summary-preview' ).empty();
const parse = response.parse;
if ( !parse || !parse.parsedsummary ) {
return;
}
$summaryPreview.append(
mw.message( 'summary-preview' ).parse(),
' ',
$( '<span>' ).addClass( 'comment' ).html( parenthesesWrap( parse.parsedsummary ) )
);
}
/**
* Wrap a string in parentheses.
*
* @private
* @param {string} str
* @return {string}
*/
function parenthesesWrap( str ) {
if ( str === '' ) {
return str;
}
// There is no equivalent to rawParams
return mw.message( 'parentheses' ).escaped()
// Specify a function as the replacement,
// so that "$" characters in str are not interpreted.
.replace( '$1', () => str );
}
/**
* Show status indicators.
*
* @private
* @param {Array} indicators
*/
function showIndicators( indicators ) {
// eslint-disable-next-line no-jquery/no-map-util
indicators = $.map( indicators, ( indicator, name ) => $( '<div>' )
.addClass( 'mw-indicator' )
.attr( 'id', mw.util.escapeIdForAttribute( 'mw-indicator-' + name ) )
.html( indicator )
.get( 0 ) );
if ( indicators.length ) {
mw.hook( 'wikipage.indicators' ).fire( $( indicators ) );
}
// Add whitespace between the <div>s because
// they get displayed with display: inline-block
const newList = [];
indicators.forEach( ( indicator ) => {
newList.push( indicator, document.createTextNode( '\n' ) );
} );
$( '.mw-indicators' ).empty().append( newList );
}
/**
* Show the templates used.
*
* The formatting here repeats what is done in includes/TemplatesOnThisPageFormatter.php
*
* @private
* @param {Array} templates List of template titles.
*/
function showTemplates( templates ) {
// The .templatesUsed div can be empty, if no templates are in use.
// In that case, we have to create the required structure.
const $parent = $( '.templatesUsed' );
// Find or add the explanation text (the toggler for collapsing).
let $explanation = $parent.find( '.mw-templatesUsedExplanation p' );
if ( $explanation.length === 0 ) {
$explanation = $( '<p>' );
$parent.append( $( '<div>' )
.addClass( 'mw-templatesUsedExplanation' )
.append( $explanation ) );
}
// Find or add the list. The makeCollapsible() method is called on this
// in resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
let $list = $parent.find( 'ul' );
if ( $list.length === 0 ) {
$list = $( '<ul>' ).addClass( [ 'mw-editfooter-list', 'mw-collapsible', 'mw-made-collapsible' ] );
$parent.append( $list );
}
if ( templates.length === 0 ) {
$explanation.msg( 'templatesusedpreview', 0 );
$list.empty();
return;
}
// Fetch info about all templates, batched because API is limited to 50 at a time.
$parent.addClass( 'mw-preview-loading-elements-loading' );
const batchSize = 50;
const requests = [];
for ( let batch = 0; batch < templates.length; batch += batchSize ) {
// Build a list of template names for this batch.
const titles = templates
.slice( batch, batch + batchSize )
.map( ( template ) => template.title );
requests.push( api.post( {
action: 'query',
format: 'json',
formatversion: 2,
titles: titles,
prop: 'info',
// @todo Do we need inlinkcontext here?
inprop: 'linkclasses|protection',
intestactions: 'edit'
} ) );
}
$.when( ...requests ).done( function () {
const templatesAllInfo = [];
// For the first batch, empty the list in preparation for either adding new items or not needing to.
for ( let r = 0; r < arguments.length; r++ ) {
// Response is either the whole argument, or the 0th element of it.
const response = arguments[ r ][ 0 ] || arguments[ r ];
const templatesInfo = ( response.query && response.query.pages ) || [];
templatesInfo.forEach( ( ti ) => {
templatesAllInfo.push( {
title: mw.Title.newFromText( ti.title ),
apiData: ti
} );
} );
}
// Sort alphabetically.
templatesAllInfo.sort( ( t1, t2 ) => {
// Compare titles with the same rules of Title::compare() in PHP.
if ( t1.title.getNamespaceId() !== t2.title.getNamespaceId() ) {
return t1.title.getNamespaceId() - t2.title.getNamespaceId();
} else {
return t1.title.getMain() === t2.title.getMain() ?
0 :
t1.title.getMain() < t2.title.getMain() ? -1 : 1;
}
} );
// Add new template list, and update the list header.
const $listNew = $( '<ul>' );
addItemToTemplateListPromise( $listNew, templatesAllInfo, 0 )
.then( () => {
$list.html( $listNew.html() );
} );
$explanation.msg( 'templatesusedpreview', templatesAllInfo.length );
} ).always( () => {
$parent.removeClass( 'mw-preview-loading-elements-loading' );
} );
}
/**
* Recursive function to add a template link to the list of templates in use.
* This is useful because addItemToTemplateList() might need to make extra API requests to fetch
* messages, but we don't want to send parallel requests for these (because they're often the
* for the same messages).
*
* @private
* @param {jQuery} $list The `<ul>` to add the item to.
* @param {Object} templatesInfo All templates' info, sorted by namespace and title.
* @param {number} templateIndex The current item in templatesInfo (0-indexed).
* @return {jQuery.Promise}
*/
function addItemToTemplateListPromise( $list, templatesInfo, templateIndex ) {
return addItemToTemplateList( $list, templatesInfo[ templateIndex ] ).then( () => {
if ( templatesInfo[ templateIndex + 1 ] !== undefined ) {
return addItemToTemplateListPromise( $list, templatesInfo, templateIndex + 1 );
}
} );
}
/**
* Create list item with relevant links for the given template, and add it to the $list.
*
* @private
* @param {jQuery} $list The `<ul>` to add the item to.
* @param {Object} template Template info with which to construct the `<li>`.
* @return {jQuery.Promise}
*/
function addItemToTemplateList( $list, template ) {
const editable = template.apiData.ns >= 0;
const canEdit = editable && template.apiData.actions.edit !== undefined;
const linkClasses = template.apiData.linkclasses || [];
if ( template.apiData.missing !== undefined && template.apiData.known === undefined ) {
linkClasses.push( 'new' );
}
const $baseLink = $( '<a>' )
// Additional CSS classes (e.g. link colors) used for links to this template.
// The following classes might be used here:
// * new
// * mw-redirect
// * any added by the GetLinkColours hook
.addClass( linkClasses );
const $link = $baseLink.clone()
.attr( 'href', template.title.getUrl() )
.text( template.title.getPrefixedText() );
if ( editable ) {
const $editLink = $baseLink.clone()
.attr( 'href', template.title.getUrl( { action: 'edit' } ) )
.append( mw.msg( canEdit ? 'editlink' : 'viewsourcelink' ) );
const wordSep = mw.message( 'word-separator' ).escaped();
return getRestrictionsText( template.apiData.protection || [] )
.then( ( restrictionsList ) => {
// restrictionsList is a comma-separated parentheses-wrapped localized list of restriction level names.
const editLinkParens = parenthesesWrap( $editLink[ 0 ].outerHTML );
const $li = $( '<li>' ).append( $link, wordSep, editLinkParens, wordSep, restrictionsList );
$list.append( $li );
} );
} else {
$list.append( $( '<li>' ).append( $link ) );
return $.Deferred().resolve( '' );
}
}
/**
* Get a localized string listing the restriction levels for a template.
*
* This should match the logic from TemplatesOnThisPageFormatter::getRestrictionsText().
*
* @private
* @param {Array} restrictions Set of protection info objects from the inprop=protection API.
* @return {jQuery.Promise}
*/
function getRestrictionsText( restrictions ) {
let msg = '';
if ( !restrictions ) {
return $.Deferred().resolve( msg );
}
// Record other restriction levels, in case it's protected for others.
const restrictionLevels = [];
restrictions.forEach( ( r ) => {
if ( r.type !== 'edit' ) {
return;
}
if ( r.level === 'sysop' ) {
msg = mw.msg( 'template-protected' );
} else if ( r.level === 'autoconfirmed' ) {
msg = mw.msg( 'template-semiprotected' );
} else {
restrictionLevels.push( r.level );
}
} );
// If sysop or autoconfirmed, use that.
if ( msg !== '' ) {
return $.Deferred().resolve( msg );
}
// Otherwise, if the edit restriction isn't one of the backwards-compatible ones,
// use the (possibly custom) restriction-level-* messages.
const msgs = [];
restrictionLevels.forEach( ( level ) => {
msgs.push( 'restriction-level-' + level );
} );
if ( msgs.length === 0 ) {
return $.Deferred().resolve( '' );
}
// Custom restriction levels don't have their messages loaded, so we have to do that.
return api.loadMessagesIfMissing( msgs ).then( () => {
const localizedMessages = msgs.map(
// Messages that can be used here include:
// * restriction-level-sysop
// * restriction-level-autoconfirmed
( m ) => mw.message( m ).parse()
);
// There's no commaList in JS, so just join with commas (doesn't handle the last item).
return parenthesesWrap( localizedMessages.join( mw.msg( 'comma-separator' ) ) );
} );
}
/**
* Show the language links (Vector-specific).
* TODO: Doesn't work in vector-2022 (maybe it doesn't need to?)
*
* @private
* @param {Array} langLinks
*/
function showLanguageLinks( langLinks ) {
const newList = langLinks.map( ( langLink ) => {
const bcp47 = mw.language.bcp47( langLink.lang );
// eslint-disable-next-line mediawiki/class-doc
return $( '<li>' )
.addClass( 'interlanguage-link interwiki-' + langLink.lang )
.append( $( '<a>' )
.attr( {
href: langLink.url,
title: langLink.title + ' - ' + langLink.langname,
lang: bcp47,
hreflang: bcp47
} )
.text( langLink.autonym )
);
} );
const $list = $( '#p-lang ul' ),
$parent = $list.parent();
$list.detach().empty().append( newList ).prependTo( $parent );
}
/**
* Parse preview response and show a warning at the top of the preview.
*
* @private
* @param {Object} config
* @param {Object} response
*/
function showPreviewNotes( config, response ) {
const arrow = $( document.body ).css( 'direction' ) === 'rtl' ? '←' : '→';
const $previewHeader = $( '<div>' )
.addClass( 'previewnote' )
.append( $( '<h2>' )
.attr( 'id', 'mw-previewheader' )
// TemplateSandbox will insert an HTML string here.
.append( config.previewHeader )
);
const warningContentElement = $( '<div>' )
.append(
// TemplateSandbox will insert a jQuery here.
config.previewNote,
' ',
$( '<span>' )
.addClass( 'mw-continue-editing' )
.append( $( '<a>' )
.attr( 'href', '#' + config.$formNode.attr( 'id' ) )
.text( arrow + ' ' + mw.msg( 'continue-editing' ) )
),
response.parse.parsewarningshtml.map( ( warning ) => $( '<p>' ).append( warning ) )
)[ 0 ];
const warningMessageElement = util.messageBox(
warningContentElement,
'warning'
);
$previewHeader.append( warningMessageElement );
config.$previewNode.prepend( $previewHeader );
}
/**
* Show an error message in place of a preview.
*
* @private
* @param {Object} config
* @param {jQuery} $message
*/
function showError( config, $message ) {
const errorContentElement = $( '<div>' )
.append(
$( '<strong>' ).text( mw.msg( 'previewerrortext' ) ),
$message
)[ 0 ];
const errorMessageElement = util.messageBox( errorContentElement, 'error' );
errorMessageElement.classList.add( 'mw-page-preview-error' );
config.$previewNode.hide().before( errorMessageElement );
if ( config.$diffNode ) {
config.$diffNode.hide();
}
}
/**
* Update the various bits of the page based on the response.
*
* @private
* @param {Object} config
* @param {Object} response
*/
function handleParseResponse( config, response ) {
let $content;
// Js config variables and modules.
if ( response.parse.jsconfigvars ) {
mw.config.set( response.parse.jsconfigvars );
}
if ( response.parse.modules ) {
mw.loader.load( response.parse.modules.concat(
response.parse.modulestyles
) );
}
// Indicators.
showIndicators( response.parse.indicators );
// Display title.
if ( response.parse.displaytitle ) {
$( '#firstHeadingTitle' ).html( response.parse.displaytitle );
}
// Categories.
if ( response.parse.categorieshtml ) {
$content = $( $.parseHTML( response.parse.categorieshtml ) );
mw.hook( 'wikipage.categories' ).fire( $content );
$( '.catlinks[data-mw="interface"]' ).replaceWith( $content );
}
// Table of contents.
if ( response.parse.sections ) {
/**
* Fired when dynamic changes have been made to the table of contents.
*
* @event ~'wikipage.tableOfContents'
* @memberof Hooks
* @param {Object[]} sections Metadata about each section, as returned by
* [API:Parse]{@link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext}.
*/
mw.hook( 'wikipage.tableOfContents' ).fire(
response.parse.hidetoc ? [] : response.parse.sections
);
}
// Templates.
if ( response.parse.templates ) {
showTemplates( response.parse.templates );
}
// Limit report.
if ( response.parse.limitreporthtml ) {
$( '.limitreport' ).html( response.parse.limitreporthtml )
.find( '.mw-collapsible' ).makeCollapsible();
}
// Language links.
if ( response.parse.langlinks && mw.config.get( 'skin' ) === 'vector' ) {
showLanguageLinks( response.parse.langlinks );
}
if ( !response.parse.text ) {
return;
}
// Remove any previous preview
config.$previewNode.children( '.mw-parser-output' ).remove();
// Remove preview note, if present (added by Live Preview, etc.).
config.$previewNode.find( '.previewnote' ).remove();
if ( config.isLivePreview ) {
showPreviewNotes( config, response );
}
$content = $( $.parseHTML( response.parse.text ) );
config.$previewNode.append( $content ).show();
mw.hook( 'wikipage.content' ).fire( $content );
}
/**
* Get the unresolved promise of the preview request.
*
* @private
* @param {Object} config
* @param {string|number} section
* @return {jQuery.Promise}
*/
function getParseRequest( config, section ) {
const params = {
formatversion: 2,
action: 'parse',
summary: config.summary,
prop: ''
};
params[ config.titleParam ] = config.title;
if ( !config.showDiff ) {
params[ config.textParam ] = config.$textareaNode.textSelection( 'getContents' );
Object.assign( params, {
prop: 'text|indicators|displaytitle|modules|jsconfigvars|categorieshtml|sections|templates|langlinks|limitreporthtml|parsewarningshtml',
pst: true,
preview: true,
sectionpreview: section !== '',
disableeditsection: true,
useskin: mw.config.get( 'skin' ),
uselang: mw.config.get( 'wgUserLanguage' )
} );
if ( mw.config.get( 'wgUserVariant' ) ) {
params.variant = mw.config.get( 'wgUserVariant' );
}
}
if ( section === 'new' ) {
params.section = 'new';
params.sectiontitle = params.summary;
delete params.summary;
}
Object.assign( params, config.parseParams );
return api.post( params, { headers: { 'Promise-Non-Write-API-Action': 'true' } } );
}
/**
* Get the required <table> structure for displaying diffs.
*
* @return {jQuery}
*/
function getDiffTable() {
return $( '<table>' ).addClass( 'diff' ).append(
$( '<col>' ).addClass( 'diff-marker' ),
$( '<col>' ).addClass( 'diff-content' ),
$( '<col>' ).addClass( 'diff-marker' ),
$( '<col>' ).addClass( 'diff-content' ),
$( '<thead>' ).append(
$( '<tr>' ).addClass( 'diff-title' ).append(
$( '<td>' )
.attr( 'colspan', 2 )
.addClass( 'diff-otitle diff-side-deleted' )
.text( mw.msg( 'currentrev' ) ),
$( '<td>' )
.attr( 'colspan', 2 )
.addClass( 'diff-ntitle diff-side-added' )
.text( mw.msg( 'yourtext' ) )
)
),
$( '<tbody>' )
);
}
/**
* Show the diff from the response.
*
* @private
* @param {Object} config
* @param {Object} response
*/
function handleDiffResponse( config, response ) {
const $table = getDiffTable();
config.$diffNode
.hide()
.empty()
.append( $table );
const diff = response.compare.bodies;
if ( diff.main ) {
$table.find( 'tbody' ).html( diff.main );
mw.hook( 'wikipage.diff' ).fire( $table );
} else {
// The diff is empty.
const $tableCell = $( '<td>' )
.attr( 'colspan', 4 )
.addClass( 'diff-notice' )
.append(
$( '<div>' )
.addClass( 'mw-diff-empty' )
.text( mw.msg( 'diff-empty' ) )
);
$table.find( 'tbody' )
.empty()
.append(
$( '<tr>' ).append( $tableCell )
);
}
config.$diffNode.show();
}
/**
* Get the unresolved promise of the diff request.
*
* @private
* @param {Object} config
* @param {string|number} section
* @param {boolean} pageExists
* @return {jQuery.Promise}
*/
function getDiffRequest( config, section, pageExists ) {
let contents = config.$textareaNode.textSelection( 'getContents' ),
sectionTitle = config.summary;
if ( section === 'new' ) {
// T293930: Hack to show live diff for new section creation.
// We concatenate the section heading with the edit box text and pass it to
// the diff API as the full input text. This is roughly what the server-side
// does when difference is requested for section edit.
// The heading is always prepended, we do not bother with editing old rev
// at this point (`?action=edit&oldid=xxx§ion=new`) -- which will require
// mid-text insertion of the section -- because creation of new section is only
// possible on latest revision.
// The section heading text is unconditionally wrapped in <h2> heading and
// ends with double newlines, except when it's empty. This is for parity with the
// server-side rendering of the same case.
sectionTitle = sectionTitle === '' ? '' : '== ' + sectionTitle + ' ==\n\n';
// Prepend section heading to section text.
contents = sectionTitle + contents;
}
const params = {
action: 'compare',
fromtitle: config.title,
totitle: config.title,
toslots: 'main',
// Remove trailing whitespace for consistency with EditPage diffs.
// TODO trimEnd() when we can use that.
'totext-main': contents.replace( /\s+$/, '' ),
'tocontentmodel-main': mw.config.get( 'wgPageContentModel' ),
topst: true,
slots: 'main',
uselang: mw.config.get( 'wgUserLanguage' )
};
if ( mw.config.get( 'wgUserVariant' ) ) {
params.variant = mw.config.get( 'wgUserVariant' );
}
if ( section ) {
params[ 'tosection-main' ] = section;
}
if ( !pageExists ) {
params.fromslots = 'main';
params[ 'fromcontentmodel-main' ] = mw.config.get( 'wgPageContentModel' );
params[ 'fromtext-main' ] = '';
}
return api.post( params );
}
/**
* Get the selectors of elements that should be grayed out while the preview is being generated.
*
* @memberof module:mediawiki.page.preview
* @return {string[]}
* @stable
*/
function getLoadingSelectors() {
return [
// Main
'.mw-indicators',
'#firstHeading',
'#wikiPreview',
'#wikiDiff',
'#catlinks',
'#p-lang',
// Editing-related
'.templatesUsed',
'.limitreport',
'.mw-summary-preview',
'.hiddencats'
];
}
/**
* Fetch and display a preview of the current editing area.
*
* @memberof module:mediawiki.page.preview
* @param {Object} config Configuration options.
* @param {jQuery} [config.$previewNode=$( '#wikiPreview' )] Where the preview should be displayed.
* @param {jQuery} [config.$diffNode=$( '#wikiDiff' )] Where diffs should be displayed (if showDiff is set).
* @param {jQuery} [config.$formNode=$( '#editform' )] The form node.
* @param {jQuery} [config.$textareaNode=$( '#wpTextbox1' )] The edit form's textarea.
* @param {jQuery} [config.$spinnerNode=$( '.mw-spinner-preview' )] The loading indicator. This will
* be shown/hidden accordingly while waiting for the XMLHttpRequest to complete.
* Ignored if it doesn't exist in the document and `createSpinner` is false.
* @param {string} [config.summary=null] The edit summary. If no value is given, the summary is
* fetched from `$( '#wpSummaryWidget' )`.
* @param {boolean} [config.showDiff=false] Shows a diff in the preview area instead of the content.
* @param {boolean} [config.isLivePreview=false] Instructs the module to replicate the
* server-side preview as much as possible. Specifically:
* - Before initiating the preview, some alerts and error messages at the top of the page will
* be removed, and the browser will scroll to the preview.
* - After finishing the preview, a reminder that it's only a preview, or an error message in
* case a request has failed, will be shown at the top of the preview.
* @param {Node|Node[]|jQuery|string} [config.previewHeader=null] Content of `<h2>` element at
* the top of the preview notes. Required if `isLivePreview` is true.
* @param {Node|Node[]|jQuery|string} [config.previewNote=null] Main text of the first preview
* note. Required if `isLivePreview` is true.
* @param {string} [config.title=mw.config.get( 'wgPageName' )] The title of the page being previewed.
* @param {string} [config.titleParam='title'] Name of the parse API parameter to pass `title` to.
* @param {string} [config.textParam='text'] Name of the parse API parameter to pass the content
* of `$textareaNode` to. Ignored if `showDiff` is true.
* @param {Object} [config.parseParams=null] Additional parse API parameters. This can override
* any parameter set by the module.
* @param {module:mediawiki.page.preview~responseHandler} [config.responseHandler=null] Callback
* to run right after the API responses are received. This allows the config and response
* objects to be modified before the preview is shown.
* @param {boolean} [config.createSpinner=false] Creates `$spinnerNode` and inserts it before
* `$previewNode` if one doesn't already exist and the module `jquery.spinner` is loaded.
* @param {string[]} [config.loadingSelectors=getLoadingSelectors()] An array of query selectors
* (i.e. '#catlinks') that should be grayed out while the preview is being generated.
* @return {jQuery.Promise|undefined} jQuery.Promise or `undefined` if no `$textareaNode` was provided in the config.
* @fires Hooks~'wikipage.categories'
* @fires Hooks~'wikipage.content'
* @fires Hooks~'wikipage.diff'
* @fires Hooks~'wikipage.indicators'
* @fires Hooks~'wikipage.tableOfContents'
* @stable
*/
function doPreview( config ) {
config = Object.assign( {
$previewNode: $( '#wikiPreview' ),
$diffNode: $( '#wikiDiff' ),
$formNode: $( '#editform' ),
$textareaNode: $( '#wpTextbox1' ),
$spinnerNode: $( '.mw-spinner-preview' ),
summary: null,
showDiff: false,
isLivePreview: false,
previewHeader: null,
previewNote: null,
title: mw.config.get( 'wgPageName' ),
titleParam: 'title',
textParam: 'text',
parseParams: null,
responseHandler: null,
createSpinner: false,
loadingSelectors: getLoadingSelectors()
}, config );
const section = config.$formNode.find( '[name="wpSection"]' ).val();
if ( !config.$textareaNode || config.$textareaNode.length === 0 ) {
return;
}
// Fetch edit summary, if not already given.
if ( !config.summary ) {
const $summaryWidget = $( '#wpSummaryWidget' );
if ( $summaryWidget.length ) {
config.summary = OO.ui.infuse( $summaryWidget ).getValue();
}
}
if ( config.isLivePreview ) {
// Not shown during normal preview, to be removed if present
$( '.mw-newarticletext, .mw-page-preview-error' ).remove();
// Show #wikiPreview if it's hidden to be able to scroll to it.
// (If it is hidden, it's also empty, so nothing changes in the rendering.)
config.$previewNode.show();
// Jump to where the preview will appear
config.$previewNode[ 0 ].scrollIntoView();
}
// Show or create the spinner if possible.
if ( config.$spinnerNode && config.$spinnerNode.length ) {
config.$spinnerNode.show();
} else if ( config.createSpinner ) {
if ( mw.loader.getState( 'jquery.spinner' ) === 'ready' ) {
config.$spinnerNode = $.createSpinner( {
size: 'large',
type: 'block'
} )
.addClass( 'mw-spinner-preview' )
.insertBefore( config.$previewNode );
} else {
mw.log.warn( 'createSpinner requires the module jquery.spinner' );
}
}
// Gray out the 'copy elements' while we wait for a response.
const $loadingElements = $( config.loadingSelectors.join( ',' ) );
$loadingElements.addClass( [ 'mw-preview-loading-elements', 'mw-preview-loading-elements-loading' ] );
// Acquire a temporary user username before previewing or diffing, so that signatures and
// user-related magic words display the temp user instead of IP user in the preview. (T331397)
const tempUserNamePromise = mw.user.acquireTempUserName();
let diffRequest;
const parseRequest = tempUserNamePromise.then( () => getParseRequest( config, section ) );
if ( config.showDiff ) {
config.$previewNode.hide();
// Add the diff node if it doesn't exist (directly after the preview node).
if ( config.$diffNode.length === 0 && config.$previewNode.length > 0 ) {
const rtlDir = $( '#wpTextbox1' ).attr( 'dir' ) === 'rtl';
const alignStart = rtlDir ? 'right' : 'left';
config.$diffNode = $( '<div>' )
.attr( 'id', 'wikiDiff' )
// The following classes are used here:
// * diff-editfont-monospace
// * diff-editfont-sans-serif
// * diff-editfont-serif
.addClass( 'diff-editfont-' + mw.user.options.get( 'editfont' ) )
// The following classes are used here:
// * diff-contentalign-left
// * diff-contentalign-right
.addClass( 'diff-contentalign-' + alignStart );
config.$previewNode.after( config.$diffNode );
}
// Hide the table of contents, in case it was previously shown after previewing.
mw.hook( 'wikipage.tableOfContents' ).fire( [] );
// The compare API returns an error if the title doesn't exist and fromtext is not
// specified. So we have to account for the possibility that the page was created or
// deleted after the user started editing. Luckily the parse API returns pageid so we
// can wait for that.
// TODO: Show "Warning: This page was deleted after you started editing!"?
diffRequest = parseRequest.then( ( parseResponse ) => getDiffRequest( config, section, parseResponse.parse.pageid !== 0 ) );
} else if ( config.$diffNode ) {
config.$diffNode.hide();
}
return $.when( parseRequest, diffRequest )
.done( ( parseResponse, diffResponse ) => {
if ( config.responseHandler ) {
/**
* @callback module:mediawiki.page.preview~responseHandler
* @param {Object} config Options for live preview API
* @param {Object} parseResponse Parse API response
* @param {Object} [diffResponse] Compare API response
*/
if ( config.showDiff ) {
config.responseHandler( config, parseResponse[ 0 ], diffResponse[ 0 ] );
} else {
config.responseHandler( config, parseResponse[ 0 ] );
}
}
showEditSummary( config.$formNode, parseResponse[ 0 ] );
if ( config.showDiff ) {
handleDiffResponse( config, diffResponse[ 0 ] );
} else {
handleParseResponse( config, parseResponse[ 0 ] );
}
mw.hook( 'wikipage.editform' ).fire( config.$formNode );
} )
.fail( ( _code, result ) => {
if ( config.isLivePreview ) {
// This just shows the error for whatever request failed first
showError( config, api.getErrorMessage( result ) );
}
} )
.always( () => {
if ( config.$spinnerNode && config.$spinnerNode.length ) {
config.$spinnerNode.hide();
}
$loadingElements.removeClass( 'mw-preview-loading-elements-loading' );
} );
}
/**
* Fetch and display a preview of the current editing area.
*
* @example
* var preview = require( 'mediawiki.page.preview' );
* preview.doPreview();
*
* @exports mediawiki.page.preview
*/
module.exports = {
doPreview: doPreview,
getLoadingSelectors: getLoadingSelectors
};
}() );