mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-28 18:21:50 +00:00
ffafc1a752
* 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
290 lines
8.9 KiB
JavaScript
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
|
|
};
|