'use strict';
var
$pageContainer, linksController, $readAsWikiPage,
pageThreads,
lastControllerScrollOffset,
featuresEnabled = mw.config.get( 'wgDiscussionToolsFeaturesEnabled' ) || {},
createMemoryStorage = require( './createMemoryStorage.js' ),
storage = createMemoryStorage( mw.storage.session ),
Parser = require( './Parser.js' ),
ThreadItemSet = require( './ThreadItemSet.js' ),
CommentDetails = require( './CommentDetails.js' ),
ReplyLinksController = require( './ReplyLinksController.js' ),
logger = require( './logger.js' ),
utils = require( './utils.js' ),
highlighter = require( './highlighter.js' ),
topicSubscriptions = require( './topicsubscriptions.js' ),
pageHandlersSetup = false,
pageDataCache = {};
mw.messages.set( require( './controller/contLangMessages.json' ) );
/**
* Get an MW API instance
*
* @return {mw.Api} API instance
*/
function getApi() {
return new mw.Api( {
parameters: {
formatversion: 2,
uselang: mw.config.get( 'wgUserLanguage' )
}
} );
}
/**
* Get various pieces of page metadata.
*
* This method caches responses. If you call it again with the same parameters, you'll get the exact
* same Promise object, and no API request will be made.
*
* @param {string} pageName Page title
* @param {number} oldId Revision ID
* @return {jQuery.Promise}
*/
function getPageData( pageName, oldId ) {
var api = getApi();
pageDataCache[ pageName ] = pageDataCache[ pageName ] || {};
if ( pageDataCache[ pageName ][ oldId ] ) {
return pageDataCache[ pageName ][ oldId ];
}
var lintPromise, transcludedFromPromise;
if ( oldId ) {
lintPromise = api.get( {
action: 'query',
list: 'linterrors',
lntcategories: 'fostered',
lntlimit: 1,
lnttitle: pageName
} ).then( function ( response ) {
return OO.getProp( response, 'query', 'linterrors' ) || [];
} );
transcludedFromPromise = api.get( {
action: 'discussiontoolspageinfo',
page: pageName,
oldid: oldId
} ).then( function ( response ) {
return OO.getProp( response, 'discussiontoolspageinfo', 'transcludedfrom' ) || {};
} );
} else {
lintPromise = $.Deferred().resolve( [] ).promise();
transcludedFromPromise = $.Deferred().resolve( {} ).promise();
}
var veMetadataPromise = api.get( {
action: 'visualeditor',
paction: 'metadata',
page: pageName
} ).then( function ( response ) {
return OO.getProp( response, 'visualeditor' ) || [];
} );
pageDataCache[ pageName ][ oldId ] = $.when( lintPromise, transcludedFromPromise, veMetadataPromise )
.then( function ( linterrors, transcludedfrom, metadata ) {
return {
linterrors: linterrors,
transcludedfrom: transcludedfrom,
metadata: metadata
};
}, function () {
// Clear on failure
pageDataCache[ pageName ][ oldId ] = null;
// Let caller handle the error
return $.Deferred().rejectWith( this, arguments );
} );
return pageDataCache[ pageName ][ oldId ];
}
/**
* Check if a given thread item on a page can be replied to
*
* @param {string} pageName Page title
* @param {number} oldId Revision ID
* @param {ThreadItem} threadItem Thread item
* @return {jQuery.Promise} Resolved with a CommentDetails object if the comment appears on the page.
* Rejects with error data if the comment is transcluded, or there are lint errors on the page.
*/
function checkThreadItemOnPage( pageName, oldId, threadItem ) {
var isNewTopic = threadItem.id === utils.NEW_TOPIC_COMMENT_ID;
return getPageData( pageName, oldId )
.then( function ( response ) {
var metadata = response.metadata,
lintErrors = response.linterrors,
transcludedFrom = response.transcludedfrom;
if ( !isNewTopic ) {
// First look for data by the thread item's ID. If not found, also look by name.
// Data by ID may not be found due to differences in headings (e.g. T273413, T275821),
// or if a thread item's parent changes.
// Data by name might be combined from two or more thread items, which would only allow us to
// treat them both as transcluded from unknown source, unless we check ID first.
var isTranscludedFrom = transcludedFrom[ threadItem.id ];
if ( isTranscludedFrom === undefined ) {
isTranscludedFrom = transcludedFrom[ threadItem.name ];
}
if ( isTranscludedFrom === undefined ) {
// The thread item wasn't found when generating the "transcludedfrom" data,
// so we don't know where the reply should be posted. Just give up.
return $.Deferred().reject( 'discussiontools-commentid-notfound-transcludedfrom', { errors: [ {
code: 'discussiontools-commentid-notfound-transcludedfrom',
html: mw.message( 'discussiontools-error-comment-disappeared' ).parse() +
' ' +
mw.message( 'discussiontools-error-comment-disappeared-reload' ).parse()
} ] } ).promise();
} else if ( isTranscludedFrom ) {
var mwTitle = isTranscludedFrom === true ? null : mw.Title.newFromText( isTranscludedFrom );
// If this refers to a template rather than a subpage, we never want to edit it
var follow = mwTitle && mwTitle.getNamespaceId() !== mw.config.get( 'wgNamespaceIds' ).template;
var transcludedErrMsg;
if ( follow ) {
transcludedErrMsg = mw.message(
'discussiontools-error-comment-is-transcluded-title',
mwTitle.getPrefixedText()
).parse();
} else if ( metadata.canEdit ) {
// If the user can edit, advise them to use the edit button
transcludedErrMsg = mw.message(
'discussiontools-error-comment-is-transcluded',
// eslint-disable-next-line no-jquery/no-global-selector
$( '#ca-edit' ).text()
).parse();
} else {
// Otherwise, tell them why they can't edit
transcludedErrMsg = metadata.notices[ 'permissions-error' ];
}
return $.Deferred().reject( 'comment-is-transcluded', { errors: [ {
data: {
transcludedFrom: isTranscludedFrom,
follow: follow
},
code: 'comment-is-transcluded',
html: transcludedErrMsg
} ] } ).promise();
}
if ( lintErrors.length ) {
// We currently only request the first error
var lintType = lintErrors[ 0 ].category;
return $.Deferred().reject( 'lint', { errors: [ {
code: 'lint',
html: mw.message( 'discussiontools-error-lint',
'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Lint_errors/' + lintType,
'https://www.mediawiki.org/wiki/Special:MyLanguage/Help_talk:Lint_errors/' + lintType,
mw.util.getUrl( pageName, { action: 'edit', lintid: lintErrors[ 0 ].lintId } ) ).parse()
} ] } ).promise();
}
}
if ( !metadata.canEdit ) {
return $.Deferred().reject( 'permissions-error', { errors: [ {
code: 'permissions-error',
html: metadata.notices[ 'permissions-error' ]
} ] } ).promise();
}
return new CommentDetails( pageName, oldId, metadata.notices );
} );
}
/**
* Get a promise which resolves with editor checkbox data
*
* @param {string} pageName Page title
* @param {number} oldId Revision ID
* @return {jQuery.Promise} See ve.init.mw.ArticleTargetLoader#createCheckboxFields
*/
function getCheckboxesPromise( pageName, oldId ) {
return getPageData(
pageName,
oldId
).then( function ( pageData ) {
var data = pageData.metadata,
checkboxesDef = {};
mw.messages.set( data.checkboxesMessages );
// Only show the watch checkbox for now
if ( 'wpWatchthis' in data.checkboxesDef ) {
checkboxesDef.wpWatchthis = data.checkboxesDef.wpWatchthis;
// Override the label with a more verbose one to distinguish this from topic subscriptions (T290712)
checkboxesDef.wpWatchthis[ 'label-message' ] = 'discussiontools-replywidget-watchthis';
}
return mw.loader.using( 'ext.visualEditor.targetLoader' ).then( function () {
return mw.libs.ve.targetLoader.createCheckboxFields( checkboxesDef );
} );
// TODO: createCheckboxField doesn't make links in the label open in a new
// window as that method currently lives in ve.utils
} );
}
/**
* Initialize Discussion Tools features
*
* @param {jQuery} $container Page container
* @param {Object} [state] Page state data object
* @param {string} [state.repliedTo] The comment ID that was just replied to
*/
function init( $container, state ) {
var
activeCommentId = null,
activeController = null,
// Loads later to avoid circular dependency
CommentController = require( './CommentController.js' ),
NewTopicController = require( './NewTopicController.js' );
// Lazy-load postEdit module, may be required later (on desktop)
mw.loader.using( 'mediawiki.action.view.postEdit' );
if ( OO.ui.isMobile() && mw.config.get( 'skin' ) === 'minerva' ) {
// For compatibility with Minerva click tracking (T295490)
$container.find( '.section-heading' ).attr( 'data-event-name', 'talkpage.section' );
}
$pageContainer = $container;
linksController = new ReplyLinksController( $pageContainer );
var parser = new Parser( require( './parser/data.json' ) );
var commentNodes = $pageContainer[ 0 ].querySelectorAll( '[data-mw-comment]' );
pageThreads = ThreadItemSet.static.newFromAnnotatedNodes( commentNodes, $pageContainer[ 0 ], parser );
if ( featuresEnabled.topicsubscription ) {
topicSubscriptions.initTopicSubscriptions( $container, pageThreads );
}
if ( mw.config.get( 'skin' ) === 'minerva' ) {
// Mobile overflow menu
mw.loader.using( [ 'oojs-ui-widgets', 'oojs-ui.styles.icons-editing-core' ] ).then( function () {
$container.find( '.ext-discussiontools-init-section-ellipsisButton' ).each( function () {
var buttonMenu = OO.ui.infuse( this, { menu: {
horizontalPosition: 'end',
items: [
new OO.ui.MenuOptionWidget( {
data: 'edit',
icon: 'edit',
label: mw.msg( 'skin-view-edit' )
} )
]
} } );
buttonMenu.getMenu().on( 'choose', function ( menuOption ) {
switch ( menuOption.getData() ) {
case 'edit':
// Click the hidden section-edit link
buttonMenu.$element.closest( '.ext-discussiontools-init-section' ).find( '.mw-editsection > a' ).trigger( 'click' );
break;
}
} );
} );
$container.find( '.ext-discussiontools-init-section-bar' ).on( 'click', function ( e ) {
// Don't toggle section when clicking on bar
e.stopPropagation();
} );
} );
if ( !$readAsWikiPage ) {
// Read as wiki page button, copied from renderReadAsWikiPageButton in Minerva
$readAsWikiPage = $( '