mediawiki-extensions-Discus.../modules/controller.js
Bartosz Dziewoński ffafc1a752 Another attempt to fix page re-initialization after updating
* Run the 'wikipage.content' hook before our initialization.
  This way collapsible elements and other changes to the page layout
  are processed before we measure where the new reply is and try to
  highlight and scroll it into view. (T252903)

* Remove the code dealing with 'mw-parser-output' and 'dt-init-done'.
  This was needed to avoid initializing twice on the same element,
  which can't happen now. (T254807)

This is much closer to the original approach Ed proposed in
I05a3c766668999f05cfe06473652429025595196 before I changed it:
https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DiscussionTools/+/550693/7..8

Bug: T252903
Bug: T254807
Change-Id: Ibc3fcbd3c92c8eceda19b68cee9e69f6e92456f5
2020-06-24 22:56:40 +02:00

290 lines
8.9 KiB
JavaScript

'use strict';
var
api = new mw.Api( { parameters: { formatversion: 2 } } ),
$pageContainer,
parser = require( './parser.js' ),
utils = require( './utils.js' ),
pageDataCache = {};
mw.messages.set( require( './controller/contLangMessages.json' ) );
function autoSignWikitext( wikitext ) {
var matches;
wikitext = wikitext.trim();
if ( ( matches = wikitext.match( /~{3,5}$/ ) ) ) {
// Sig detected, check it has the correct number of tildes
if ( matches[ 0 ].length !== 4 ) {
wikitext = wikitext.slice( 0, -matches[ 0 ].length ) + '~~~~';
}
// Otherwise 4 tilde signature is left alone,
// with any adjacent characters
} else {
// No sig, append separator and sig
wikitext += mw.msg( 'discussiontools-signature-prefix' ) + '~~~~';
}
return wikitext;
}
function sanitizeWikitextLinebreaks( wikitext ) {
return wikitext
.replace( /\r/g, '\n' )
.replace( /\n+/g, '\n' );
}
function traverseNode( parent, thread ) {
// Loads later to avoid circular dependency
var CommentController = require( './CommentController.js' );
parent.replies.forEach( function ( comment ) {
if ( comment.type === 'comment' ) {
// eslint-disable-next-line no-new
new CommentController( $pageContainer, comment, thread );
}
traverseNode( comment, thread );
} );
}
function highlight( comment ) {
var padding = 5,
// $container must be position:relative/absolute
$container = OO.ui.getDefaultOverlay(),
containerRect = $container[ 0 ].getBoundingClientRect(),
nativeRange, rect,
$highlight = $( '<div>' ).addClass( 'dt-init-highlight' );
nativeRange = utils.getNativeRange( comment );
rect = RangeFix.getBoundingClientRect( nativeRange );
$highlight.css( {
top: rect.top - containerRect.top - padding,
left: rect.left - containerRect.left - padding,
width: rect.width + ( padding * 2 ),
height: rect.height + ( padding * 2 )
} );
$container.prepend( $highlight );
OO.ui.Element.static.scrollIntoView( $highlight[ 0 ], { padding: { top: 10, bottom: 10 } } ).then( function () {
setTimeout( function () {
$highlight.addClass( 'dt-init-highlight-fade' );
setTimeout( function () {
$highlight.remove();
}, 500 );
}, 500 );
} );
}
function commentsById( comments ) {
var byId = {};
comments.forEach( function ( comment ) {
byId[ comment.id ] = comment;
} );
return byId;
}
/**
* Get the Parsoid document HTML and metadata needed to edit this page from the API.
*
* 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.
*
* TODO: Resolve the naming conflict between this raw "pageData" from the API, and the
* plain object "pageData" that gets attached to parsoidData.
*
* @param {string} pageName Page title
* @param {number} oldId Revision ID
* @return {jQuery.Promise}
*/
function getPageData( pageName, oldId ) {
var lintPromise;
pageDataCache[ pageName ] = pageDataCache[ pageName ] || {};
if ( pageDataCache[ pageName ][ oldId ] ) {
return pageDataCache[ pageName ][ oldId ];
}
lintPromise = api.get( {
action: 'query',
list: 'linterrors',
lntcategories: 'fostered',
lntlimit: 1,
lnttitle: pageName
} ).then( function ( response ) {
return OO.getProp( response, 'query', 'linterrors' ) || [];
} );
pageDataCache[ pageName ][ oldId ] = mw.loader.using( 'ext.visualEditor.targetLoader' ).then( function () {
var pageDataPromise = mw.libs.ve.targetLoader.requestPageData(
'visual', pageName, { oldId: oldId }
);
return $.when( lintPromise, pageDataPromise ).then( function ( linterrors, pageData ) {
pageData.linterrors = linterrors;
return pageData;
} );
}, function () {
// Clear on failure
pageDataCache[ pageName ][ oldId ] = null;
} );
return pageDataCache[ pageName ][ oldId ];
}
/**
* Get the Parsoid document DOM, parse comments and threads, and find a specific comment in it.
*
* @param {string} pageName Page title
* @param {number} oldId Revision ID
* @param {string} commentId Comment ID
* @return {jQuery.Promise}
*/
function getParsoidCommentData( pageName, oldId, commentId ) {
var parsoidPageData, parsoidDoc, parsoidComments, parsoidCommentsById;
return getPageData( pageName, oldId )
.then( function ( response ) {
var data, comment, transcludedFrom, transcludedErrMsg, mwTitle, follow,
lintType,
lintErrors = response.linterrors;
data = response.visualeditor;
parsoidDoc = ve.parseXhtml( data.content );
// Remove section wrappers, they interfere with transclusion handling
mw.libs.ve.unwrapParsoidSections( parsoidDoc.body );
// Mirror VE's ve.init.mw.Target.prototype.fixBase behavior:
ve.fixBase( parsoidDoc, document, ve.resolveUrl(
// Don't replace $1 with the page name, because that'll break if
// the page name contains a slash
mw.config.get( 'wgArticlePath' ).replace( '$1', '' ),
document
) );
parsoidComments = parser.getComments( parsoidDoc.body );
parsoidPageData = {
pageName: pageName,
oldId: oldId,
startTimeStamp: data.starttimestamp,
etag: data.etag
};
// getThreads builds the tree structure, currently only
// used to set 'replies' and 'id'
parser.groupThreads( parsoidComments );
parsoidCommentsById = commentsById( parsoidComments );
comment = parsoidCommentsById[ commentId ];
if ( !comment ) {
return $.Deferred().reject( 'comment-disappeared', { errors: [ {
code: 'comment-disappeared',
html: mw.message( 'discussiontools-error-comment-disappeared' ).parse()
} ] } ).promise();
}
transcludedFrom = parser.getTranscludedFrom( comment );
if ( transcludedFrom ) {
mwTitle = transcludedFrom === true ? null : mw.Title.newFromText( transcludedFrom );
// If this refers to a template rather than a subpage, we never want to edit it
follow = mwTitle && mwTitle.getNamespaceId() !== mw.config.get( 'wgNamespaceIds' ).template;
if ( follow ) {
transcludedErrMsg = mw.message(
'discussiontools-error-comment-is-transcluded-title',
mwTitle.getPrefixedText()
).parse();
} else {
transcludedErrMsg = mw.message(
'discussiontools-error-comment-is-transcluded',
// eslint-disable-next-line no-jquery/no-global-selector
$( '#ca-edit' ).text()
).parse();
}
return $.Deferred().reject( 'comment-is-transcluded', { errors: [ {
data: {
transcludedFrom: transcludedFrom,
follow: follow
},
code: 'comment-is-transcluded',
html: transcludedErrMsg
} ] } ).promise();
}
if ( lintErrors.length ) {
// We currently only request the first error
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();
}
return {
comment: parsoidCommentsById[ commentId ],
doc: parsoidDoc,
pageData: parsoidPageData
};
} );
}
function getCheckboxesPromise( pageData ) {
return getPageData(
pageData.pageName,
pageData.oldId
).then( function ( response ) {
var data = response.visualeditor,
checkboxesDef = {};
mw.messages.set( data.checkboxesMessages );
// Only show the watch checkbox for now
if ( 'wpWatchthis' in data.checkboxesDef ) {
checkboxesDef.wpWatchthis = data.checkboxesDef.wpWatchthis;
}
// targetLoader was loaded by getPageData
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
} );
}
function init( $container, state ) {
var
pageComments, pageThreads, pageCommentsById,
repliedToComment;
$pageContainer = $container;
pageComments = parser.getComments( $pageContainer[ 0 ] );
pageThreads = parser.groupThreads( pageComments );
pageCommentsById = commentsById( pageComments );
$pageContainer.removeClass( 'dt-init-replylink-open' );
pageThreads.forEach( function ( thread ) {
traverseNode( thread, thread );
} );
// For debugging
mw.dt.pageThreads = pageThreads;
if ( state.repliedTo ) {
// Find the comment we replied to, then highlight the last reply
repliedToComment = pageCommentsById[ state.repliedTo ];
highlight( repliedToComment.replies[ repliedToComment.replies.length - 1 ] );
}
// Preload the Parsoid document.
// TODO: Isn't this too early to load it? We will only need it if the user tries replying...
getPageData(
mw.config.get( 'wgRelevantPageName' ),
mw.config.get( 'wgCurRevisionId' )
);
}
module.exports = {
init: init,
getParsoidCommentData: getParsoidCommentData,
getCheckboxesPromise: getCheckboxesPromise,
autoSignWikitext: autoSignWikitext,
sanitizeWikitextLinebreaks: sanitizeWikitextLinebreaks
};