mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-09-24 19:08:16 +00:00
6f404e5ce2
Previously, we only built the Parsoid document once (on page load) and kept it around forever. Every time we tried to post a reply, it was added to this document, even if it wasn't saved due to some error. This resulted in duplicate replies when the user managed to actually save. Now we only keep around the HTML string and some metadata fetched from the API, and rebuild the actual document every time before adding a reply. Bug: T245333 Change-Id: Ib1c344a7d613cdf67644aa243147c5e699c2c1e7
265 lines
7.8 KiB
JavaScript
265 lines
7.8 KiB
JavaScript
'use strict';
|
|
|
|
var
|
|
parser = require( 'ext.discussionTools.parser' ),
|
|
modifier = require( 'ext.discussionTools.modifier' ),
|
|
pageDataCache = {},
|
|
$pageContainer,
|
|
scrollPadding = { top: 10, bottom: 10 },
|
|
config = require( './config.json' ),
|
|
replyWidgetPromise = config.useVisualEditor ?
|
|
mw.loader.using( 'ext.discussionTools.ReplyWidgetVisual' ) :
|
|
mw.loader.using( 'ext.discussionTools.ReplyWidgetPlain' );
|
|
|
|
function setupComment( comment ) {
|
|
var $replyLink, widgetPromise, newListItem,
|
|
$tsNode = $( comment.range.endContainer );
|
|
|
|
// Is it possible to have a heading nested in a thread?
|
|
if ( comment.type !== 'comment' ) {
|
|
return;
|
|
}
|
|
|
|
$replyLink = $( '<a>' )
|
|
.addClass( 'dt-init-replylink' )
|
|
.text( mw.msg( 'discussiontools-replylink' ) )
|
|
.on( 'click', function () {
|
|
var $link = $( this );
|
|
|
|
$link.addClass( 'dt-init-replylink-active' );
|
|
// TODO: Allow users to use multiple reply widgets simlutaneously
|
|
// Currently as all widgets share the same Parsoid doc, this could
|
|
// cause problems.
|
|
$pageContainer.addClass( 'dt-init-replylink-open' );
|
|
|
|
if ( !widgetPromise ) {
|
|
newListItem = modifier.addListItem( comment );
|
|
$( newListItem ).text( mw.msg( 'discussiontools-replywidget-loading' ) );
|
|
widgetPromise = replyWidgetPromise.then( function () {
|
|
var
|
|
ReplyWidget = config.useVisualEditor ?
|
|
require( 'ext.discussionTools.ReplyWidgetVisual' ) :
|
|
require( 'ext.discussionTools.ReplyWidgetPlain' ),
|
|
replyWidget = new ReplyWidget(
|
|
comment
|
|
);
|
|
|
|
replyWidget.on( 'teardown', function () {
|
|
$link.removeClass( 'dt-init-replylink-active' );
|
|
$pageContainer.removeClass( 'dt-init-replylink-open' );
|
|
$( newListItem ).hide();
|
|
} );
|
|
|
|
$( newListItem ).empty().append( replyWidget.$element );
|
|
return replyWidget;
|
|
}, function () {
|
|
$link.removeClass( 'dt-init-replylink-active' );
|
|
$pageContainer.removeClass( 'dt-init-replylink-open' );
|
|
} );
|
|
}
|
|
widgetPromise.then( function ( replyWidget ) {
|
|
$( newListItem ).show();
|
|
replyWidget.setup();
|
|
replyWidget.scrollElementIntoView( { padding: scrollPadding } );
|
|
replyWidget.focus();
|
|
} );
|
|
} );
|
|
|
|
$tsNode.after( $replyLink );
|
|
}
|
|
|
|
function traverseNode( parent ) {
|
|
parent.replies.forEach( function ( comment ) {
|
|
setupComment( comment );
|
|
traverseNode( comment );
|
|
} );
|
|
}
|
|
|
|
function autoSignWikitext( wikitext ) {
|
|
wikitext = wikitext.trim();
|
|
if ( wikitext.slice( -4 ) !== '~~~~' ) {
|
|
wikitext += ' ~~~~';
|
|
}
|
|
return wikitext;
|
|
}
|
|
|
|
function postReply( widget, parsoidData ) {
|
|
var root, summary,
|
|
comment = parsoidData.comment,
|
|
pageData = parsoidData.pageData,
|
|
newParsoidItem = modifier.addListItem( comment );
|
|
|
|
widget.insertNewNodes( newParsoidItem );
|
|
|
|
root = comment;
|
|
while ( root && root.type !== 'heading' ) {
|
|
root = root.parent;
|
|
}
|
|
|
|
summary = '/* ' + root.range.startContainer.innerText + ' */ ' +
|
|
mw.msg( 'discussiontools-defaultsummary-reply' );
|
|
|
|
return mw.libs.ve.targetSaver.saveDoc(
|
|
parsoidData.doc,
|
|
{
|
|
page: pageData.pageName,
|
|
oldid: pageData.oldId,
|
|
summary: summary,
|
|
basetimestamp: pageData.baseTimeStamp,
|
|
starttimestamp: pageData.startTimeStamp,
|
|
etag: pageData.etag,
|
|
assert: mw.user.isAnon() ? 'anon' : 'user',
|
|
assertuser: mw.user.getName() || undefined,
|
|
// This appears redundant currently, but as editing / new-topics get added, we'll expand it
|
|
dttags: [ 'discussiontools', 'discussiontools-reply', 'discussiontools-' + widget.mode ].join( ',' )
|
|
}
|
|
);
|
|
}
|
|
|
|
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 = document.createRange();
|
|
nativeRange.setStart( comment.range.startContainer, comment.range.startOffset );
|
|
nativeRange.setEnd( comment.range.endContainer, comment.range.endOffset );
|
|
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 )
|
|
} );
|
|
|
|
setTimeout( function () {
|
|
$highlight.addClass( 'dt-init-highlight-fade' );
|
|
setTimeout( function () {
|
|
$highlight.remove();
|
|
}, 500 );
|
|
}, 500 );
|
|
|
|
$container.prepend( $highlight );
|
|
}
|
|
|
|
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.
|
|
*
|
|
* @param {string} pageName Page title
|
|
* @param {number} oldId Revision ID
|
|
* @return {jQuery.Promise}
|
|
*/
|
|
function getPageData( pageName, oldId ) {
|
|
pageDataCache[ pageName ] = pageDataCache[ pageName ] || {};
|
|
if ( pageDataCache[ pageName ][ oldId ] ) {
|
|
return pageDataCache[ pageName ][ oldId ];
|
|
}
|
|
pageDataCache[ pageName ][ oldId ] = mw.loader.using( 'ext.visualEditor.targetLoader' ).then( function () {
|
|
return mw.libs.ve.targetLoader.requestPageData(
|
|
'visual', pageName, { oldId: oldId }
|
|
);
|
|
}, 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, from a comment parsed in the local document
|
|
* @return {jQuery.Promise}
|
|
*/
|
|
function getParsoidCommentData( pageName, oldId, commentId ) {
|
|
var parsoidPageData, parsoidDoc, parsoidComments, parsoidCommentsById;
|
|
|
|
return getPageData( pageName, oldId )
|
|
.then( function ( response ) {
|
|
var data = response.visualeditor;
|
|
// TODO: error handling
|
|
parsoidDoc = ve.createDocumentFromHtml( data.content );
|
|
parsoidComments = parser.getComments( parsoidDoc.body );
|
|
|
|
parsoidPageData = {
|
|
pageName: pageName,
|
|
oldId: oldId
|
|
};
|
|
parsoidPageData.baseTimeStamp = data.basetimestamp;
|
|
parsoidPageData.startTimeStamp = data.starttimestamp;
|
|
parsoidPageData.etag = data.etag;
|
|
|
|
// getThreads build the tree structure, currently only
|
|
// used to set 'replies'
|
|
parser.groupThreads( parsoidComments );
|
|
parsoidCommentsById = commentsById( parsoidComments );
|
|
|
|
if ( !parsoidCommentsById[ commentId ] ) {
|
|
throw new Error( 'Could not find comment in Parsoid HTML' );
|
|
}
|
|
|
|
return {
|
|
comment: parsoidCommentsById[ commentId ],
|
|
doc: parsoidDoc,
|
|
pageData: parsoidPageData
|
|
};
|
|
} );
|
|
}
|
|
|
|
function init( $container, state ) {
|
|
var
|
|
pageComments, pageThreads, pageCommentsById,
|
|
repliedToComment;
|
|
|
|
state = state || {};
|
|
$pageContainer = $container;
|
|
pageComments = parser.getComments( $pageContainer[ 0 ] );
|
|
pageThreads = parser.groupThreads( pageComments );
|
|
pageCommentsById = commentsById( pageComments );
|
|
|
|
pageThreads.forEach( traverseNode );
|
|
|
|
$pageContainer.addClass( 'dt-init-done' );
|
|
$pageContainer.removeClass( 'dt-init-replylink-open' );
|
|
|
|
// 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( 'wgRevisionId' )
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
init: init,
|
|
getParsoidCommentData: getParsoidCommentData,
|
|
postReply: postReply,
|
|
autoSignWikitext: autoSignWikitext
|
|
};
|