'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 = $( '