mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-29 14:33:12 +00:00
87145e818a
saveProcessDeferred will only exist if content was actually added. This wasn't causing any functional issues, just an error in the console after you save. This issue was introduced via I1dccc3c. Bug: T344382 Change-Id: Icb0c29b64d960308343ddbb3f9d2ee641552b814
341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
require( './EditCheckContextItem.js' );
|
|
require( './EditCheckInspector.js' );
|
|
|
|
mw.editcheck = {};
|
|
|
|
mw.editcheck.config = require( './config.json' );
|
|
|
|
mw.editcheck.accountShouldSeeEditCheck = function ( config ) {
|
|
// account status:
|
|
// loggedin, loggedout, or any-other-value meaning 'both'
|
|
// we'll count temporary users as "logged out" by using isNamed here
|
|
if ( config.account === 'loggedout' && mw.user.isNamed() ) {
|
|
return false;
|
|
}
|
|
if ( config.account === 'loggedin' && !mw.user.isNamed() ) {
|
|
return false;
|
|
}
|
|
if ( config.maximumEditcount && mw.config.get( 'wgUserEditCount', 0 ) > config.maximumEditcount ) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Find added content in the document model that might need a reference
|
|
*
|
|
* @param {ve.dm.DocumentModel} documentModel Document model
|
|
* @param {boolean} [includeReferencedContent] Include content ranges that already
|
|
* have a reference.
|
|
* @return {ve.dm.Selection[]} Content ranges that might need a reference
|
|
*/
|
|
mw.editcheck.findAddedContentNeedingReference = function ( documentModel, includeReferencedContent ) {
|
|
if ( mw.config.get( 'wgNamespaceNumber' ) !== mw.config.get( 'wgNamespaceIds' )[ '' ] ) {
|
|
return [];
|
|
}
|
|
|
|
if ( !documentModel.completeHistory.getLength() ) {
|
|
return [];
|
|
}
|
|
var operations;
|
|
try {
|
|
operations = documentModel.completeHistory.squash().transactions[ 0 ].operations;
|
|
} catch ( err ) {
|
|
// TransactionSquasher can sometimes throw errors; until T333710 is
|
|
// fixed just count this as not needing a reference.
|
|
mw.errorLogger.logError( err, 'error.visualeditor' );
|
|
return [];
|
|
}
|
|
|
|
var ranges = [];
|
|
var offset = 0;
|
|
var endOffset = documentModel.getDocumentRange().end;
|
|
operations.every( function ( op ) {
|
|
if ( op.type === 'retain' ) {
|
|
offset += op.length;
|
|
} else if ( op.type === 'replace' ) {
|
|
var insertedRange = new ve.Range( offset, offset + op.insert.length );
|
|
offset += op.insert.length;
|
|
// 1. Only trigger if the check is a pure insertion, with no adjacent content removed (T340088)
|
|
if ( op.remove.length === 0 ) {
|
|
ve.batchPush(
|
|
ranges,
|
|
// 2. Only fully inserted paragraphs (ranges that cover the whole node) (T345121)
|
|
mw.editcheck.getContentRanges( documentModel, insertedRange, true )
|
|
);
|
|
}
|
|
}
|
|
// Reached the end of the doc / start of internal list, stop searching
|
|
return offset < endOffset;
|
|
} );
|
|
var addedTextRanges = ranges.filter( function ( range ) {
|
|
var minimumCharacters = mw.editcheck.config.addReference.minimumCharacters;
|
|
// 3. Check that at least minimumCharacters characters have been inserted sequentially
|
|
if ( range.getLength() >= minimumCharacters ) {
|
|
// 4. Exclude any ranges that already contain references
|
|
if ( !includeReferencedContent ) {
|
|
for ( var i = range.start; i < range.end; i++ ) {
|
|
if ( documentModel.data.isElementData( i ) && documentModel.data.getType( i ) === 'mwReference' ) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// 5. Exclude any ranges that aren't at the document root (i.e. image captions, table cells)
|
|
var branchNode = documentModel.getBranchNodeFromOffset( range.start );
|
|
if ( branchNode.getParent() !== documentModel.attachedRoot ) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
} );
|
|
|
|
return addedTextRanges.map( function ( range ) {
|
|
return new ve.dm.LinearSelection( range );
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Return the content ranges (content branch node interiors) contained within a range
|
|
*
|
|
* For a content branch node entirely contained within the range, its entire interior
|
|
* range will be included. For a content branch node overlapping with the range boundary,
|
|
* only the covered part of its interior range will be included.
|
|
*
|
|
* @param {ve.dm.Document} documentModel The documentModel to search
|
|
* @param {ve.Range} range The range to include
|
|
* @param {boolean} covers Only include ranges which cover the whole of their node
|
|
* @return {ve.Range[]} The contained content ranges (content branch node interiors)
|
|
*/
|
|
mw.editcheck.getContentRanges = function ( documentModel, range, covers ) {
|
|
var ranges = [];
|
|
documentModel.selectNodes( range, 'branches' ).forEach( function ( spec ) {
|
|
if (
|
|
spec.node.canContainContent() && (
|
|
!covers || (
|
|
!spec.range || // an empty range means the node is covered
|
|
spec.range.equalsSelection( spec.nodeRange )
|
|
)
|
|
)
|
|
) {
|
|
ranges.push( spec.range || spec.nodeRange );
|
|
}
|
|
} );
|
|
return ranges;
|
|
};
|
|
|
|
mw.editcheck.rejections = [];
|
|
|
|
mw.editcheck.getRejectionReasons = function () {
|
|
return mw.editcheck.rejections;
|
|
};
|
|
|
|
mw.editcheck.refCheckShown = false;
|
|
|
|
if ( mw.config.get( 'wgVisualEditorConfig' ).editCheckTagging ) {
|
|
mw.hook( 've.activationComplete' ).add( function () {
|
|
var target = ve.init.target;
|
|
|
|
function getRefNodes() {
|
|
// The firstNodes list is a numerically indexed array of reference nodes in the document.
|
|
// The list is append only, and removed references are set to undefined in place.
|
|
// To check if a new reference is being published, we just need to know if a reference
|
|
// with an index beyond the initial list (initLength) is still set.
|
|
var internalList = target.getSurface().getModel().getDocument().getInternalList();
|
|
var group = internalList.getNodeGroup( 'mwReference/' );
|
|
return group ? group.firstNodes || [] : [];
|
|
}
|
|
|
|
var initLength = getRefNodes().length;
|
|
target.saveFields.vetags = function () {
|
|
var refNodes = getRefNodes();
|
|
var newLength = refNodes.length;
|
|
var newNodesInDoc = false;
|
|
for ( var i = initLength; i < newLength; i++ ) {
|
|
if ( refNodes[ i ] ) {
|
|
newNodesInDoc = true;
|
|
break;
|
|
}
|
|
}
|
|
var tags = [];
|
|
if ( newNodesInDoc ) {
|
|
tags.push( 'editcheck-newreference' );
|
|
}
|
|
if ( mw.editcheck.refCheckShown ) {
|
|
tags.push( 'editcheck-references-activated' );
|
|
}
|
|
return tags.join( ',' );
|
|
};
|
|
} );
|
|
mw.hook( 've.deactivationComplete' ).add( function () {
|
|
var target = ve.init.target;
|
|
delete target.saveFields.vetags;
|
|
} );
|
|
}
|
|
|
|
if (
|
|
( mw.config.get( 'wgVisualEditorConfig' ).editCheck && mw.editcheck.accountShouldSeeEditCheck( mw.editcheck.config.addReference ) ) ||
|
|
// ecenable will bypass normal account-status checks as well:
|
|
new URL( location.href ).searchParams.get( 'ecenable' )
|
|
) {
|
|
var saveProcessDeferred;
|
|
mw.hook( 've.preSaveProcess' ).add( function ( saveProcess, target ) {
|
|
var surface = target.getSurface();
|
|
|
|
// clear rejection-reasons between runs of the save process, so only the last one counts
|
|
mw.editcheck.rejections.length = 0;
|
|
|
|
var selections = mw.editcheck.findAddedContentNeedingReference( surface.getModel().getDocument() );
|
|
|
|
if ( selections.length ) {
|
|
mw.editcheck.refCheckShown = true;
|
|
|
|
var surfaceView = surface.getView();
|
|
var toolbar = target.getToolbar();
|
|
var reviewToolbar = new ve.ui.PositionedTargetToolbar( target, target.toolbarConfig );
|
|
reviewToolbar.setup( [
|
|
{
|
|
name: 'back',
|
|
type: 'bar',
|
|
include: [ 'editCheckBack' ]
|
|
},
|
|
// Placeholder toolbar groups
|
|
// TODO: Make a proper TitleTool?
|
|
{
|
|
name: 'title',
|
|
type: 'bar',
|
|
include: []
|
|
},
|
|
{
|
|
name: 'save',
|
|
// TODO: MobileArticleTarget should ignore 'align'
|
|
align: OO.ui.isMobile() ? 'before' : 'after',
|
|
type: 'bar',
|
|
include: [ 'showSaveDisabled' ]
|
|
}
|
|
], surface );
|
|
|
|
reviewToolbar.items[ 1 ].$element.removeClass( 'oo-ui-toolGroup-empty' );
|
|
reviewToolbar.items[ 1 ].$group.append(
|
|
$( '<span>' ).addClass( 've-ui-editCheck-toolbar-title' ).text( ve.msg( 'editcheck-dialog-title' ) )
|
|
);
|
|
if ( OO.ui.isMobile() ) {
|
|
reviewToolbar.$element.addClass( 've-init-mw-mobileArticleTarget-toolbar' );
|
|
}
|
|
target.toolbar.$element.before( reviewToolbar.$element );
|
|
target.toolbar = reviewToolbar;
|
|
|
|
var selection = selections[ 0 ];
|
|
var highlightNodes = surfaceView.getDocument().selectNodes( selection.getCoveringRange(), 'branches' ).map( function ( spec ) {
|
|
return spec.node;
|
|
} );
|
|
|
|
surfaceView.drawSelections( 'editCheck', [ ve.ce.Selection.static.newFromModel( selection, surfaceView ) ] );
|
|
surfaceView.setReviewMode( true, highlightNodes );
|
|
toolbar.toggle( false );
|
|
target.onContainerScroll();
|
|
|
|
saveProcess.next( function () {
|
|
saveProcessDeferred = ve.createDeferred();
|
|
var fragment = surface.getModel().getFragment( selection, true );
|
|
|
|
var context = surface.getContext();
|
|
|
|
// Select the found content to correctly the context on desktop
|
|
fragment.select();
|
|
// Deactivate to prevent selection suppressing mobile context
|
|
surface.getView().deactivate();
|
|
|
|
context.addPersistentSource( {
|
|
embeddable: false,
|
|
data: {
|
|
fragment: fragment,
|
|
saveProcessDeferred: saveProcessDeferred
|
|
},
|
|
name: 'editCheckReferences'
|
|
} );
|
|
|
|
// Once the context is positioned, clear the selection
|
|
setTimeout( function () {
|
|
surface.getModel().setNullSelection();
|
|
} );
|
|
|
|
return saveProcessDeferred.promise().then( function ( data ) {
|
|
context.removePersistentSource( 'editCheckReferences' );
|
|
|
|
surfaceView.drawSelections( 'editCheck', [] );
|
|
surfaceView.setReviewMode( false );
|
|
|
|
reviewToolbar.$element.remove();
|
|
toolbar.toggle( true );
|
|
target.toolbar = toolbar;
|
|
target.onContainerScroll();
|
|
|
|
// Check the user inserted a citation
|
|
if ( data && data.action ) {
|
|
if ( data.action !== 'reject' ) {
|
|
mw.notify( ve.msg( 'editcheck-dialog-addref-success-notify' ), { type: 'success' } );
|
|
} else if ( data.reason ) {
|
|
mw.editcheck.rejections.push( data.reason );
|
|
}
|
|
var delay = ve.createDeferred();
|
|
// If they inserted, wait 2 seconds on desktop before showing save dialog
|
|
setTimeout( function () {
|
|
delay.resolve();
|
|
}, !OO.ui.isMobile() && data.action !== 'reject' ? 2000 : 0 );
|
|
return delay.promise();
|
|
} else {
|
|
return ve.createDeferred().reject().promise();
|
|
}
|
|
} );
|
|
} );
|
|
}
|
|
} );
|
|
mw.hook( 've.deactivationComplete' ).add( function () {
|
|
if ( saveProcessDeferred ) {
|
|
saveProcessDeferred.reject();
|
|
}
|
|
} );
|
|
}
|
|
|
|
ve.ui.EditCheckBack = function VeUiEditCheckBack() {
|
|
// Parent constructor
|
|
ve.ui.EditCheckBack.super.apply( this, arguments );
|
|
|
|
this.setDisabled( false );
|
|
};
|
|
OO.inheritClass( ve.ui.EditCheckBack, ve.ui.Tool );
|
|
ve.ui.EditCheckBack.static.name = 'editCheckBack';
|
|
ve.ui.EditCheckBack.static.icon = 'previous';
|
|
ve.ui.EditCheckBack.static.autoAddToCatchall = false;
|
|
ve.ui.EditCheckBack.static.autoAddToGroup = false;
|
|
ve.ui.EditCheckBack.static.title =
|
|
OO.ui.deferMsg( 'visualeditor-backbutton-tooltip' );
|
|
ve.ui.EditCheckBack.prototype.onSelect = function () {
|
|
var context = this.toolbar.getSurface().getContext();
|
|
if ( context.inspector ) {
|
|
context.inspector.close();
|
|
} else {
|
|
context.items[ 0 ].close();
|
|
}
|
|
this.setActive( false );
|
|
};
|
|
ve.ui.EditCheckBack.prototype.onUpdateState = function () {
|
|
this.setDisabled( false );
|
|
};
|
|
ve.ui.toolFactory.register( ve.ui.EditCheckBack );
|
|
|
|
ve.ui.EditCheckSaveDisabled = function VeUiEditCheckSaveDisabled() {
|
|
// Parent constructor
|
|
ve.ui.EditCheckSaveDisabled.super.apply( this, arguments );
|
|
};
|
|
OO.inheritClass( ve.ui.EditCheckSaveDisabled, ve.ui.MWSaveTool );
|
|
ve.ui.EditCheckSaveDisabled.static.name = 'showSaveDisabled';
|
|
ve.ui.EditCheckSaveDisabled.static.autoAddToCatchall = false;
|
|
ve.ui.EditCheckSaveDisabled.static.autoAddToGroup = false;
|
|
ve.ui.EditCheckSaveDisabled.prototype.onUpdateState = function () {
|
|
this.setDisabled( true );
|
|
};
|
|
|
|
ve.ui.toolFactory.register( ve.ui.EditCheckSaveDisabled );
|