mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-23 14:06:52 +00:00
Edit check API
Change-Id: Ic5504eb2fe8d1d3f22e88abe1dd88790bdfd8b9c
This commit is contained in:
parent
d0a57f5197
commit
d69d366469
98
editcheck/modules/AddReferenceEditCheck.js
Normal file
98
editcheck/modules/AddReferenceEditCheck.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
mw.editcheck.AddReferenceEditCheck = function MWAddReferenceEditCheck( config ) {
|
||||
// Parent constructor
|
||||
mw.editcheck.AddReferenceEditCheck.super.call( this, config );
|
||||
};
|
||||
|
||||
OO.inheritClass( mw.editcheck.AddReferenceEditCheck, mw.editcheck.BaseEditCheck );
|
||||
|
||||
mw.editcheck.AddReferenceEditCheck.static.name = 'addReference';
|
||||
|
||||
mw.editcheck.AddReferenceEditCheck.static.description = ve.msg( 'editcheck-dialog-addref-description' );
|
||||
|
||||
mw.editcheck.AddReferenceEditCheck.prototype.onBeforeSave = function ( diff ) {
|
||||
const documentModel = diff.documentModel;
|
||||
const ranges = this.getModifiedRangesFromDiff( diff ).filter( ( range ) => {
|
||||
// 4. Exclude any ranges that already contain references
|
||||
for ( let 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)
|
||||
const branchNode = documentModel.getBranchNodeFromOffset( range.start );
|
||||
if ( branchNode.getParent() !== documentModel.attachedRoot ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} );
|
||||
return ranges.map( ( range ) => {
|
||||
const fragment = diff.surface.getModel().getFragment( new ve.dm.LinearSelection( range ) );
|
||||
return new mw.editcheck.EditCheckAction( {
|
||||
highlight: fragment,
|
||||
selection: this.adjustForPunctuation( fragment.collapseToEnd() ),
|
||||
check: this
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
mw.editcheck.AddReferenceEditCheck.prototype.act = function ( choice, action, contextItem ) {
|
||||
// The complex citoid workflow means that we can't just count on a single "windowAction" here...
|
||||
const windowAction = ve.ui.actionFactory.create( 'window', contextItem.context.getSurface(), 'check' );
|
||||
switch ( choice ) {
|
||||
case 'accept':
|
||||
ve.track( 'activity.editCheckReferences', { action: 'edit-check-confirm' } );
|
||||
action.selection.select();
|
||||
|
||||
return windowAction.open( 'citoid' ).then( ( instance ) => instance.closing ).then( ( citoidData ) => {
|
||||
const citoidOrCiteDataDeferred = ve.createDeferred();
|
||||
if ( citoidData && citoidData.action === 'manual-choose' ) {
|
||||
// The plain reference dialog has been launched. Wait for the data from
|
||||
// the basic Cite closing promise instead.
|
||||
contextItem.context.getSurface().getDialogs().once( 'closing', ( win, closed, citeData ) => {
|
||||
citoidOrCiteDataDeferred.resolve( citeData );
|
||||
} );
|
||||
} else {
|
||||
// "Auto"/"re-use"/"close" means Citoid is finished and we can
|
||||
// use the data form the Citoid closing promise.
|
||||
citoidOrCiteDataDeferred.resolve( citoidData );
|
||||
}
|
||||
citoidOrCiteDataDeferred.promise().then( ( data ) => {
|
||||
if ( !data ) {
|
||||
// Reference was not inserted - re-open this context
|
||||
setTimeout( () => {
|
||||
// Deactivate again for mobile after teardown has modified selections
|
||||
contextItem.context.getSurface().getView().deactivate();
|
||||
contextItem.context.afterContextChange();
|
||||
}, 500 );
|
||||
} else {
|
||||
// Edit check inspector is already closed by this point, but
|
||||
// we need to end the workflow.
|
||||
contextItem.close( citoidData );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
case 'reject':
|
||||
ve.track( 'activity.editCheckReferences', { action: 'edit-check-reject' } );
|
||||
return windowAction.open(
|
||||
'editCheckReferencesInspector',
|
||||
{
|
||||
fragment: action.highlight,
|
||||
callback: contextItem.data.callback,
|
||||
saveProcessDeferred: contextItem.data.saveProcessDeferred
|
||||
}
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
).then( ( instance ) => {
|
||||
// contextItem.openingCitoid = false;
|
||||
return instance.closing;
|
||||
} ).then( ( data ) => {
|
||||
if ( !data ) {
|
||||
// Form was closed, re-open this context
|
||||
contextItem.context.afterContextChange();
|
||||
} else {
|
||||
contextItem.close( data );
|
||||
}
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
mw.editcheck.editCheckFactory.register( mw.editcheck.AddReferenceEditCheck );
|
118
editcheck/modules/BaseEditCheck.js
Normal file
118
editcheck/modules/BaseEditCheck.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
mw.editcheck.BaseEditCheck = function MWBaseEditCheck( config ) {
|
||||
this.config = config;
|
||||
};
|
||||
|
||||
OO.initClass( mw.editcheck.BaseEditCheck );
|
||||
|
||||
mw.editcheck.BaseEditCheck.static.onlyCoveredNodes = false;
|
||||
|
||||
mw.editcheck.BaseEditCheck.static.choices = [
|
||||
{
|
||||
action: 'accept',
|
||||
label: ve.msg( 'editcheck-dialog-action-yes' ),
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
label: ve.msg( 'editcheck-dialog-action-no' ),
|
||||
icon: 'close'
|
||||
}
|
||||
];
|
||||
|
||||
mw.editcheck.BaseEditCheck.static.description = ve.msg( 'editcheck-dialog-addref-description' );
|
||||
|
||||
/**
|
||||
* @param {mw.editcheck.Diff} diff
|
||||
* @return {mw.editcheck.EditCheckAction[]}
|
||||
*/
|
||||
mw.editcheck.BaseEditCheck.prototype.onBeforeSave = null;
|
||||
|
||||
/**
|
||||
* @param {mw.editcheck.Diff} diff
|
||||
* @return {mw.editcheck.EditCheckAction[]}
|
||||
*/
|
||||
mw.editcheck.BaseEditCheck.prototype.onDocumentChange = null;
|
||||
|
||||
/**
|
||||
* @param {string} choice `action` key from static.choices
|
||||
* @param {mw.editcheck.EditCheckAction} action
|
||||
* @param {ve.ui.EditCheckContextItem} contextItem
|
||||
*/
|
||||
mw.editcheck.BaseEditCheck.prototype.act = null;
|
||||
|
||||
mw.editcheck.BaseEditCheck.prototype.getChoices = function ( /* action */ ) {
|
||||
return this.constructor.static.choices;
|
||||
};
|
||||
mw.editcheck.BaseEditCheck.prototype.getDescription = function ( /* action */ ) {
|
||||
return this.constructor.static.description;
|
||||
};
|
||||
|
||||
mw.editcheck.BaseEditCheck.prototype.getModifiedRangesFromDiff = function ( diff ) {
|
||||
if ( !mw.editcheck.ecenable && this.config.maximumEditcount && mw.config.get( 'wgUserEditCount', 0 ) > this.config.maximumEditcount ) {
|
||||
return [];
|
||||
}
|
||||
return diff.getModifiedRanges( this.constructor.static.onlyCoveredNodes )
|
||||
.filter( ( range ) => this.shouldApplyToSection( diff, range ) && range.getLength() >= this.config.minimumCharacters );
|
||||
};
|
||||
|
||||
mw.editcheck.BaseEditCheck.prototype.shouldApplyToSection = function ( diff, range ) {
|
||||
const ignoreSections = this.config.ignoreSections || [];
|
||||
if ( ignoreSections.length === 0 && !this.config.ignoreLeadSection ) {
|
||||
// Nothing is forbidden, so everything is permitted
|
||||
return true;
|
||||
}
|
||||
const documentModel = diff.documentModel;
|
||||
const isHeading = function ( nodeType ) {
|
||||
return nodeType === 'mwHeading';
|
||||
};
|
||||
// Note: we set a limit of 1 here because otherwise this will turn around
|
||||
// to keep looking when it hits the document boundary:
|
||||
const heading = documentModel.getNearestNodeMatching( isHeading, range.start, -1, 1 );
|
||||
if ( !heading ) {
|
||||
// There's no preceding heading, so work out if we count as being in a
|
||||
// lead section. It's only a lead section if there's more headings
|
||||
// later in the document, otherwise it's just a stub article.
|
||||
return !(
|
||||
this.config.ignoreLeadSection &&
|
||||
!!documentModel.getNearestNodeMatching( isHeading, range.start, 1 )
|
||||
);
|
||||
}
|
||||
if ( ignoreSections.length === 0 ) {
|
||||
// There's nothing left to deny
|
||||
return true;
|
||||
}
|
||||
const compare = new Intl.Collator( documentModel.getLang(), { sensitivity: 'accent' } ).compare;
|
||||
const headingText = documentModel.data.getText( false, heading.getRange() );
|
||||
for ( let i = ignoreSections.length - 1; i >= 0; i-- ) {
|
||||
if ( compare( headingText, ignoreSections[ i ] ) === 0 ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
mw.editcheck.BaseEditCheck.prototype.adjustForPunctuation = function ( insertionPointFragment ) {
|
||||
if ( this.config.beforePunctuation ) {
|
||||
// TODO: Use UnicodeJS properties directly once is https://gerrit.wikimedia.org/r/c/unicodejs/+/893832 merged
|
||||
const sentenceProperties = {
|
||||
ATerm: [ 0x002E, 0x2024, 0xFE52, 0xFF0E ],
|
||||
STerm: [ 0x0021, 0x003F, 0x0589, 0x061E, 0x061F, 0x06D4, [ 0x0700, 0x0702 ], 0x07F9, 0x0837, 0x0839, 0x083D, 0x083E, 0x0964, 0x0965, 0x104A, 0x104B, 0x1362, 0x1367, 0x1368, 0x166E, 0x1735, 0x1736, 0x1803, 0x1809, 0x1944, 0x1945, [ 0x1AA8, 0x1AAB ], 0x1B5A, 0x1B5B, 0x1B5E, 0x1B5F, 0x1C3B, 0x1C3C, 0x1C7E, 0x1C7F, 0x203C, 0x203D, [ 0x2047, 0x2049 ], 0x2E2E, 0x2E3C, 0x3002, 0xA4FF, 0xA60E, 0xA60F, 0xA6F3, 0xA6F7, 0xA876, 0xA877, 0xA8CE, 0xA8CF, 0xA92F, 0xA9C8, 0xA9C9, [ 0xAA5D, 0xAA5F ], 0xAAF0, 0xAAF1, 0xABEB, 0xFE56, 0xFE57, 0xFF01, 0xFF1F, 0xFF61, 0x10A56, 0x10A57, [ 0x10F55, 0x10F59 ], 0x11047, 0x11048, [ 0x110BE, 0x110C1 ], [ 0x11141, 0x11143 ], 0x111C5, 0x111C6, 0x111CD, 0x111DE, 0x111DF, 0x11238, 0x11239, 0x1123B, 0x1123C, 0x112A9, 0x1144B, 0x1144C, 0x115C2, 0x115C3, [ 0x115C9, 0x115D7 ], 0x11641, 0x11642, [ 0x1173C, 0x1173E ], 0x11944, 0x11946, 0x11A42, 0x11A43, 0x11A9B, 0x11A9C, 0x11C41, 0x11C42, 0x11EF7, 0x11EF8, 0x16A6E, 0x16A6F, 0x16AF5, 0x16B37, 0x16B38, 0x16B44, 0x16E98, 0x1BC9F, 0x1DA88 ],
|
||||
Close: [ 0x0022, [ 0x0027, 0x0029 ], 0x005B, 0x005D, 0x007B, 0x007D, 0x00AB, 0x00BB, [ 0x0F3A, 0x0F3D ], 0x169B, 0x169C, [ 0x2018, 0x201F ], 0x2039, 0x203A, 0x2045, 0x2046, 0x207D, 0x207E, 0x208D, 0x208E, [ 0x2308, 0x230B ], 0x2329, 0x232A, [ 0x275B, 0x2760 ], [ 0x2768, 0x2775 ], 0x27C5, 0x27C6, [ 0x27E6, 0x27EF ], [ 0x2983, 0x2998 ], [ 0x29D8, 0x29DB ], 0x29FC, 0x29FD, [ 0x2E00, 0x2E0D ], 0x2E1C, 0x2E1D, [ 0x2E20, 0x2E29 ], 0x2E42, [ 0x3008, 0x3011 ], [ 0x3014, 0x301B ], [ 0x301D, 0x301F ], 0xFD3E, 0xFD3F, 0xFE17, 0xFE18, [ 0xFE35, 0xFE44 ], 0xFE47, 0xFE48, [ 0xFE59, 0xFE5E ], 0xFF08, 0xFF09, 0xFF3B, 0xFF3D, 0xFF5B, 0xFF5D, 0xFF5F, 0xFF60, 0xFF62, 0xFF63, [ 0x1F676, 0x1F678 ] ],
|
||||
SContinue: [ 0x002C, 0x002D, 0x003A, 0x055D, 0x060C, 0x060D, 0x07F8, 0x1802, 0x1808, 0x2013, 0x2014, 0x3001, 0xFE10, 0xFE11, 0xFE13, 0xFE31, 0xFE32, 0xFE50, 0xFE51, 0xFE55, 0xFE58, 0xFE63, 0xFF0C, 0xFF0D, 0xFF1A, 0xFF64 ]
|
||||
};
|
||||
const punctuationPattern = new RegExp(
|
||||
unicodeJS.charRangeArrayRegexp( [].concat(
|
||||
sentenceProperties.ATerm,
|
||||
sentenceProperties.STerm,
|
||||
sentenceProperties.Close,
|
||||
sentenceProperties.SContinue
|
||||
) )
|
||||
);
|
||||
let lastCharacter = insertionPointFragment.adjustLinearSelection( -1, 0 ).getText();
|
||||
while ( punctuationPattern.test( lastCharacter ) ) {
|
||||
insertionPointFragment = insertionPointFragment.adjustLinearSelection( -1, -1 );
|
||||
lastCharacter = insertionPointFragment.adjustLinearSelection( -1, 0 ).getText();
|
||||
}
|
||||
}
|
||||
return insertionPointFragment;
|
||||
};
|
35
editcheck/modules/ConvertReferenceEditCheck.js
Normal file
35
editcheck/modules/ConvertReferenceEditCheck.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
mw.editcheck.ConvertReferenceEditCheck = function MWConvertReferenceEditCheck( /* config */ ) {
|
||||
// Parent constructor
|
||||
mw.editcheck.ConvertReferenceEditCheck.super.apply( this, arguments );
|
||||
};
|
||||
|
||||
OO.inheritClass( mw.editcheck.ConvertReferenceEditCheck, mw.editcheck.BaseEditCheck );
|
||||
|
||||
mw.editcheck.ConvertReferenceEditCheck.static.name = 'convertReference';
|
||||
|
||||
mw.editcheck.ConvertReferenceEditCheck.prototype.onDocumentChange = function ( diff ) {
|
||||
const seenIndexes = {};
|
||||
return diff.documentModel.getNodesByType( 'mwReference' ).map( ( node ) => {
|
||||
const refModel = ve.dm.MWReferenceModel.static.newFromReferenceNode( node );
|
||||
const index = refModel.getListIndex();
|
||||
if ( seenIndexes[ index ] ) {
|
||||
return null;
|
||||
}
|
||||
seenIndexes[ index ] = true;
|
||||
const referenceNode = diff.documentModel.getInternalList().getItemNode( index );
|
||||
const href = ve.ui.CitoidReferenceContextItem.static.getConvertibleHref( referenceNode );
|
||||
if ( href ) {
|
||||
const fragment = diff.surface.getModel().getFragment( new ve.dm.LinearSelection( node.getOuterRange() ) );
|
||||
return new mw.editcheck.EditCheckAction( {
|
||||
highlight: fragment,
|
||||
selection: fragment,
|
||||
message: ve.msg( 'citoid-referencecontextitem-convert-message' ),
|
||||
check: this
|
||||
} );
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} ).filter( ( obj ) => obj );
|
||||
};
|
||||
|
||||
mw.editcheck.editCheckFactory.register( mw.editcheck.ConvertReferenceEditCheck );
|
|
@ -40,3 +40,17 @@
|
|||
padding: 2px;
|
||||
margin: -2px 0 0 -2px;
|
||||
}
|
||||
|
||||
.ve-ce-surface-selections-editCheckWarning .ve-ce-surface-selection {
|
||||
opacity: 0.2;
|
||||
|
||||
> div {
|
||||
mix-blend-mode: darken;
|
||||
// Adjust target colours to account for 50% opacity
|
||||
background: ( #fef6e7 - 0.8 * ( #fff ) ) / 0.2;
|
||||
border: 1px solid ( ( #a66200 - 0.8 * ( #fff ) ) / 0.2 );
|
||||
border-radius: 2px;
|
||||
padding: 2px;
|
||||
margin: -2px 0 0 -2px;
|
||||
}
|
||||
}
|
||||
|
|
16
editcheck/modules/EditCheckAction.js
Normal file
16
editcheck/modules/EditCheckAction.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
mw.editcheck.EditCheckAction = function MWEditCheckAction( config ) {
|
||||
this.check = config.check;
|
||||
this.highlight = config.highlight;
|
||||
this.selection = config.selection;
|
||||
this.message = config.message;
|
||||
};
|
||||
|
||||
OO.initClass( mw.editcheck.EditCheckAction );
|
||||
|
||||
mw.editcheck.EditCheckAction.prototype.getChoices = function () {
|
||||
return this.check.getChoices( this );
|
||||
};
|
||||
|
||||
mw.editcheck.EditCheckAction.prototype.getDescription = function () {
|
||||
return this.check.getDescription( this );
|
||||
};
|
|
@ -41,18 +41,17 @@ ve.ui.EditCheckContextItem.static.label = OO.ui.deferMsg( 'editcheck-dialog-addr
|
|||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.EditCheckContextItem.prototype.renderBody = function () {
|
||||
// Prompt panel
|
||||
const acceptButton = new OO.ui.ButtonWidget( {
|
||||
label: ve.msg( 'editcheck-dialog-action-yes' ),
|
||||
icon: 'check'
|
||||
} );
|
||||
const rejectButton = new OO.ui.ButtonWidget( {
|
||||
label: ve.msg( 'editcheck-dialog-action-no' ),
|
||||
icon: 'close'
|
||||
} );
|
||||
const $actions = $( '<div>' ).addClass( 've-ui-editCheckContextItem-actions' );
|
||||
|
||||
acceptButton.connect( this, { click: 'onAcceptClick' } );
|
||||
rejectButton.connect( this, { click: 'onRejectClick' } );
|
||||
this.data.action.getChoices().forEach( ( choice ) => {
|
||||
const button = new OO.ui.ButtonWidget( choice );
|
||||
button.connect( this, {
|
||||
click: () => {
|
||||
this.onChoiceClick( choice.action );
|
||||
}
|
||||
} );
|
||||
$actions.append( button.$element );
|
||||
} );
|
||||
|
||||
// HACK: Suppress close button on mobile context
|
||||
if ( this.context.isMobile() ) {
|
||||
|
@ -60,10 +59,8 @@ ve.ui.EditCheckContextItem.prototype.renderBody = function () {
|
|||
}
|
||||
|
||||
this.$body.append(
|
||||
$( '<p>' ).text( ve.msg( 'editcheck-dialog-addref-description' ) ),
|
||||
$( '<div>' ).addClass( 've-ui-editCheckContextItem-actions' ).append(
|
||||
acceptButton.$element, rejectButton.$element
|
||||
)
|
||||
$( '<p>' ).text( this.data.action.getDescription() ),
|
||||
$actions
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -75,92 +72,8 @@ ve.ui.EditCheckContextItem.prototype.close = function ( data ) {
|
|||
this.data.callback( data, this.data );
|
||||
};
|
||||
|
||||
ve.ui.EditCheckContextItem.prototype.onAcceptClick = function () {
|
||||
ve.track( 'activity.editCheckReferences', { action: 'edit-check-confirm' } );
|
||||
|
||||
const fragment = this.data.fragment;
|
||||
const windowAction = ve.ui.actionFactory.create( 'window', this.context.getSurface(), 'check' );
|
||||
|
||||
let insertionPointFragment = fragment.collapseToEnd();
|
||||
|
||||
if ( mw.editcheck.config.addReference.beforePunctuation ) {
|
||||
// TODO: Use UnicodeJS properties directly once is https://gerrit.wikimedia.org/r/c/unicodejs/+/893832 merged
|
||||
const sentenceProperties = {
|
||||
ATerm: [ 0x002E, 0x2024, 0xFE52, 0xFF0E ],
|
||||
STerm: [ 0x0021, 0x003F, 0x0589, 0x061E, 0x061F, 0x06D4, [ 0x0700, 0x0702 ], 0x07F9, 0x0837, 0x0839, 0x083D, 0x083E, 0x0964, 0x0965, 0x104A, 0x104B, 0x1362, 0x1367, 0x1368, 0x166E, 0x1735, 0x1736, 0x1803, 0x1809, 0x1944, 0x1945, [ 0x1AA8, 0x1AAB ], 0x1B5A, 0x1B5B, 0x1B5E, 0x1B5F, 0x1C3B, 0x1C3C, 0x1C7E, 0x1C7F, 0x203C, 0x203D, [ 0x2047, 0x2049 ], 0x2E2E, 0x2E3C, 0x3002, 0xA4FF, 0xA60E, 0xA60F, 0xA6F3, 0xA6F7, 0xA876, 0xA877, 0xA8CE, 0xA8CF, 0xA92F, 0xA9C8, 0xA9C9, [ 0xAA5D, 0xAA5F ], 0xAAF0, 0xAAF1, 0xABEB, 0xFE56, 0xFE57, 0xFF01, 0xFF1F, 0xFF61, 0x10A56, 0x10A57, [ 0x10F55, 0x10F59 ], 0x11047, 0x11048, [ 0x110BE, 0x110C1 ], [ 0x11141, 0x11143 ], 0x111C5, 0x111C6, 0x111CD, 0x111DE, 0x111DF, 0x11238, 0x11239, 0x1123B, 0x1123C, 0x112A9, 0x1144B, 0x1144C, 0x115C2, 0x115C3, [ 0x115C9, 0x115D7 ], 0x11641, 0x11642, [ 0x1173C, 0x1173E ], 0x11944, 0x11946, 0x11A42, 0x11A43, 0x11A9B, 0x11A9C, 0x11C41, 0x11C42, 0x11EF7, 0x11EF8, 0x16A6E, 0x16A6F, 0x16AF5, 0x16B37, 0x16B38, 0x16B44, 0x16E98, 0x1BC9F, 0x1DA88 ],
|
||||
Close: [ 0x0022, [ 0x0027, 0x0029 ], 0x005B, 0x005D, 0x007B, 0x007D, 0x00AB, 0x00BB, [ 0x0F3A, 0x0F3D ], 0x169B, 0x169C, [ 0x2018, 0x201F ], 0x2039, 0x203A, 0x2045, 0x2046, 0x207D, 0x207E, 0x208D, 0x208E, [ 0x2308, 0x230B ], 0x2329, 0x232A, [ 0x275B, 0x2760 ], [ 0x2768, 0x2775 ], 0x27C5, 0x27C6, [ 0x27E6, 0x27EF ], [ 0x2983, 0x2998 ], [ 0x29D8, 0x29DB ], 0x29FC, 0x29FD, [ 0x2E00, 0x2E0D ], 0x2E1C, 0x2E1D, [ 0x2E20, 0x2E29 ], 0x2E42, [ 0x3008, 0x3011 ], [ 0x3014, 0x301B ], [ 0x301D, 0x301F ], 0xFD3E, 0xFD3F, 0xFE17, 0xFE18, [ 0xFE35, 0xFE44 ], 0xFE47, 0xFE48, [ 0xFE59, 0xFE5E ], 0xFF08, 0xFF09, 0xFF3B, 0xFF3D, 0xFF5B, 0xFF5D, 0xFF5F, 0xFF60, 0xFF62, 0xFF63, [ 0x1F676, 0x1F678 ] ],
|
||||
SContinue: [ 0x002C, 0x002D, 0x003A, 0x055D, 0x060C, 0x060D, 0x07F8, 0x1802, 0x1808, 0x2013, 0x2014, 0x3001, 0xFE10, 0xFE11, 0xFE13, 0xFE31, 0xFE32, 0xFE50, 0xFE51, 0xFE55, 0xFE58, 0xFE63, 0xFF0C, 0xFF0D, 0xFF1A, 0xFF64 ]
|
||||
};
|
||||
const punctuationPattern = new RegExp(
|
||||
unicodeJS.charRangeArrayRegexp( [].concat(
|
||||
sentenceProperties.ATerm,
|
||||
sentenceProperties.STerm,
|
||||
sentenceProperties.Close,
|
||||
sentenceProperties.SContinue
|
||||
) )
|
||||
);
|
||||
let lastCharacter = insertionPointFragment.adjustLinearSelection( -1, 0 ).getText();
|
||||
while ( punctuationPattern.test( lastCharacter ) ) {
|
||||
insertionPointFragment = insertionPointFragment.adjustLinearSelection( -1, -1 );
|
||||
lastCharacter = insertionPointFragment.adjustLinearSelection( -1, 0 ).getText();
|
||||
}
|
||||
}
|
||||
|
||||
insertionPointFragment.select();
|
||||
|
||||
windowAction.open( 'citoid' ).then( ( instance ) => instance.closing ).then( ( citoidData ) => {
|
||||
const citoidOrCiteDataDeferred = ve.createDeferred();
|
||||
if ( citoidData && citoidData.action === 'manual-choose' ) {
|
||||
// The plain reference dialog has been launched. Wait for the data from
|
||||
// the basic Cite closing promise instead.
|
||||
this.context.getSurface().getDialogs().once( 'closing', ( win, closed, citeData ) => {
|
||||
citoidOrCiteDataDeferred.resolve( citeData );
|
||||
} );
|
||||
} else {
|
||||
// "Auto"/"re-use"/"close" means Citoid is finished and we can
|
||||
// use the data form the Citoid closing promise.
|
||||
citoidOrCiteDataDeferred.resolve( citoidData );
|
||||
}
|
||||
citoidOrCiteDataDeferred.promise().then( ( data ) => {
|
||||
if ( !data ) {
|
||||
// Reference was not inserted - re-open this context
|
||||
setTimeout( () => {
|
||||
// Deactivate again for mobile after teardown has modified selections
|
||||
this.context.getSurface().getView().deactivate();
|
||||
this.context.afterContextChange();
|
||||
}, 500 );
|
||||
} else {
|
||||
// Edit check inspector is already closed by this point, but
|
||||
// we need to end the workflow.
|
||||
this.close( citoidData );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
ve.ui.EditCheckContextItem.prototype.onRejectClick = function () {
|
||||
ve.track( 'activity.editCheckReferences', { action: 'edit-check-reject' } );
|
||||
|
||||
const windowAction = ve.ui.actionFactory.create( 'window', this.context.getSurface(), 'check' );
|
||||
windowAction.open(
|
||||
'editCheckReferencesInspector',
|
||||
{
|
||||
fragment: this.data.fragment,
|
||||
callback: this.data.callback,
|
||||
saveProcessDeferred: this.data.saveProcessDeferred
|
||||
}
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
).then( ( instance ) => {
|
||||
// this.openingCitoid = false;
|
||||
return instance.closing;
|
||||
} ).then( ( data ) => {
|
||||
if ( !data ) {
|
||||
// Form was closed, re-open this context
|
||||
this.context.afterContextChange();
|
||||
} else {
|
||||
this.close( data );
|
||||
}
|
||||
} );
|
||||
ve.ui.EditCheckContextItem.prototype.onChoiceClick = function ( choice ) {
|
||||
this.data.action.check.act( choice, this.data.action, this );
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
|
129
editcheck/modules/EditCheckDialog.js
Normal file
129
editcheck/modules/EditCheckDialog.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*!
|
||||
* VisualEditor UserInterface EditCheckDialog class.
|
||||
*
|
||||
* @copyright See AUTHORS.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find and replace dialog.
|
||||
*
|
||||
* @class
|
||||
* @extends ve.ui.ToolbarDialog
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [config] Configuration options
|
||||
*/
|
||||
ve.ui.EditCheckDialog = function VeUiEditCheckDialog( config ) {
|
||||
// Parent constructor
|
||||
ve.ui.EditCheckDialog.super.call( this, config );
|
||||
|
||||
// Pre-initialization
|
||||
this.$element.addClass( 've-ui-editCheckDialog' );
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.ui.EditCheckDialog, ve.ui.ToolbarDialog );
|
||||
|
||||
ve.ui.EditCheckDialog.static.name = 'editCheckDialog';
|
||||
|
||||
ve.ui.EditCheckDialog.static.position = 'side';
|
||||
|
||||
ve.ui.EditCheckDialog.static.size = 'medium';
|
||||
|
||||
ve.ui.EditCheckDialog.static.framed = false;
|
||||
|
||||
// // Invisible title for accessibility
|
||||
// ve.ui.EditCheckDialog.static.title =
|
||||
// OO.ui.deferMsg( 'visualeditor-find-and-replace-title' );
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.EditCheckDialog.prototype.initialize = function () {
|
||||
// Parent method
|
||||
ve.ui.EditCheckDialog.super.prototype.initialize.call( this );
|
||||
|
||||
this.updateDebounced = ve.debounce( this.update.bind( this ), 100 );
|
||||
};
|
||||
|
||||
ve.ui.EditCheckDialog.prototype.update = function () {
|
||||
const surfaceView = this.surface.getView();
|
||||
const checks = mw.editcheck.editCheckFactory.createAllByListener( 'onDocumentChange', this.surface );
|
||||
const $checks = $( '<div>' );
|
||||
const selections = [];
|
||||
checks.forEach( ( check ) => {
|
||||
$checks.append( new OO.ui.MessageWidget( {
|
||||
type: 'warning',
|
||||
label: check.message,
|
||||
framed: false
|
||||
} ).$element );
|
||||
selections.push( ve.ce.Selection.static.newFromModel( check.highlight.getSelection(), surfaceView ) );
|
||||
} );
|
||||
surfaceView.drawSelections( 'editCheckWarning', selections );
|
||||
this.$body.empty().append( $checks );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.EditCheckDialog.prototype.getSetupProcess = function ( data ) {
|
||||
return ve.ui.EditCheckDialog.super.prototype.getSetupProcess.call( this, data )
|
||||
.first( () => {
|
||||
this.surface = data.surface;
|
||||
this.surface.getModel().on( 'undoStackChange', this.updateDebounced );
|
||||
this.update();
|
||||
}, this );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.EditCheckDialog.prototype.getReadyProcess = function ( data ) {
|
||||
return ve.ui.EditCheckDialog.super.prototype.getReadyProcess.call( this, data )
|
||||
.next( () => {
|
||||
}, this );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.EditCheckDialog.prototype.getTeardownProcess = function ( data ) {
|
||||
return ve.ui.EditCheckDialog.super.prototype.getTeardownProcess.call( this, data )
|
||||
.next( () => {
|
||||
this.surface.getModel().off( 'undoStackChange', this.updateDebounced );
|
||||
}, this );
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
||||
ve.ui.windowFactory.register( ve.ui.EditCheckDialog );
|
||||
|
||||
ve.ui.commandRegistry.register(
|
||||
new ve.ui.Command(
|
||||
'editCheckDialog', 'window', 'toggle', { args: [ 'editCheckDialog' ] }
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @extends ve.ui.ToolbarDialogTool
|
||||
* @constructor
|
||||
* @param {OO.ui.ToolGroup} toolGroup
|
||||
* @param {Object} [config] Configuration options
|
||||
*/
|
||||
ve.ui.EditCheckDialogTool = function VeUiEditCheckDialogTool() {
|
||||
ve.ui.EditCheckDialogTool.super.apply( this, arguments );
|
||||
};
|
||||
OO.inheritClass( ve.ui.EditCheckDialogTool, ve.ui.ToolbarDialogTool );
|
||||
ve.ui.EditCheckDialogTool.static.name = 'editCheckDialog';
|
||||
ve.ui.EditCheckDialogTool.static.group = 'notices';
|
||||
ve.ui.EditCheckDialogTool.static.icon = 'robot';
|
||||
ve.ui.EditCheckDialogTool.static.title = 'Edit check'; // OO.ui.deferMsg( 'visualeditor-dialog-command-help-title' );
|
||||
ve.ui.EditCheckDialogTool.static.autoAddToCatchall = false;
|
||||
ve.ui.EditCheckDialogTool.static.commandName = 'editCheckDialog';
|
||||
|
||||
// Demo button for opening edit check sidebar
|
||||
// ve.ui.toolFactory.register( ve.ui.EditCheckDialogTool );
|
72
editcheck/modules/EditCheckFactory.js
Normal file
72
editcheck/modules/EditCheckFactory.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
mw.editcheck.EditCheckFactory = function MWEditEditCheckFactory() {
|
||||
// Parent constructor
|
||||
mw.editcheck.EditCheckFactory.super.call( this, this.arguments );
|
||||
|
||||
this.checksByListener = {
|
||||
onDocumentChange: [],
|
||||
onBeforeSave: []
|
||||
};
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( mw.editcheck.EditCheckFactory, OO.Factory );
|
||||
|
||||
/* Methods */
|
||||
|
||||
mw.editcheck.EditCheckFactory.prototype.register = function ( constructor, name ) {
|
||||
name = name || ( constructor.static && constructor.static.name );
|
||||
|
||||
if ( typeof name !== 'string' || name === '' ) {
|
||||
throw new Error( 'Check names must be strings and must not be empty' );
|
||||
}
|
||||
if ( !( constructor.prototype instanceof mw.editcheck.BaseEditCheck ) ) {
|
||||
throw new Error( 'Checks must be subclasses of mw.editcheck.BaseEditCheck' );
|
||||
}
|
||||
if ( this.lookup( name ) === constructor ) {
|
||||
// Don't allow double registration as it would create duplicate
|
||||
// entries in various caches.
|
||||
return;
|
||||
}
|
||||
|
||||
// Parent method
|
||||
mw.editcheck.EditCheckFactory.super.prototype.register.call( this, constructor, name );
|
||||
|
||||
if ( constructor.prototype.onDocumentChange ) {
|
||||
this.checksByListener.onDocumentChange.push( name );
|
||||
}
|
||||
if ( constructor.prototype.onBeforeSave ) {
|
||||
this.checksByListener.onBeforeSave.push( name );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of registered command names.
|
||||
*
|
||||
* @param {string} listener Listener name, 'onDocumentChange', 'onBeforeSave'
|
||||
* @return {string[]}
|
||||
*/
|
||||
mw.editcheck.EditCheckFactory.prototype.getNamesByListener = function ( listener ) {
|
||||
if ( !this.checksByListener[ listener ] ) {
|
||||
throw new Error( `Unknown listener '${ listener }'` );
|
||||
}
|
||||
return this.checksByListener[ listener ];
|
||||
};
|
||||
|
||||
mw.editcheck.EditCheckFactory.prototype.createAllByListener = function ( listener, surface ) {
|
||||
const diff = new mw.editcheck.Diff( surface );
|
||||
const newChecks = [];
|
||||
this.getNamesByListener( listener ).forEach( ( checkName ) => {
|
||||
const check = this.create( checkName, mw.editcheck.config[ checkName ] );
|
||||
const actions = check[ listener ]( diff );
|
||||
if ( actions.length > 0 ) {
|
||||
ve.batchPush( newChecks, actions );
|
||||
}
|
||||
} );
|
||||
newChecks.sort(
|
||||
( a, b ) => a.highlight.getSelection().getCoveringRange().start - b.highlight.getSelection().getCoveringRange().start
|
||||
);
|
||||
return newChecks;
|
||||
};
|
||||
|
||||
mw.editcheck.editCheckFactory = new mw.editcheck.EditCheckFactory();
|
36
editcheck/modules/TextMatchEditCheck.js
Normal file
36
editcheck/modules/TextMatchEditCheck.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
mw.editcheck.TextMatchEditCheck = function MWTextMatchEditCheck( /* config */ ) {
|
||||
// Parent constructor
|
||||
mw.editcheck.TextMatchEditCheck.super.apply( this, arguments );
|
||||
};
|
||||
|
||||
OO.inheritClass( mw.editcheck.TextMatchEditCheck, mw.editcheck.BaseEditCheck );
|
||||
|
||||
mw.editcheck.TextMatchEditCheck.static.name = 'textMatch';
|
||||
|
||||
mw.editcheck.TextMatchEditCheck.static.replacers = [
|
||||
// TODO: Load text replacement rules from community config
|
||||
{
|
||||
query: 'unfortunately',
|
||||
message: new OO.ui.HtmlSnippet( 'Use of adverbs such as "unfortunately" should usually be avoided so as to maintain an impartial tone. <a href="#">Read more</a>.' )
|
||||
}
|
||||
];
|
||||
|
||||
mw.editcheck.TextMatchEditCheck.prototype.onDocumentChange = function ( diff ) {
|
||||
const actions = [];
|
||||
this.constructor.static.replacers.forEach( ( replacer ) => {
|
||||
diff.documentModel.findText( replacer.query ).forEach( ( range ) => {
|
||||
const fragment = diff.surface.getModel().getFragment( new ve.dm.LinearSelection( range ) );
|
||||
actions.push(
|
||||
new mw.editcheck.EditCheckAction( {
|
||||
highlight: fragment,
|
||||
selection: fragment,
|
||||
message: replacer.message,
|
||||
check: this
|
||||
} )
|
||||
);
|
||||
} );
|
||||
} );
|
||||
return actions;
|
||||
};
|
||||
|
||||
mw.editcheck.editCheckFactory.register( mw.editcheck.TextMatchEditCheck );
|
|
@ -1,9 +1,14 @@
|
|||
mw.editcheck = {
|
||||
config: require( './config.json' ),
|
||||
ecenable: !!( new URL( location.href ).searchParams.get( 'ecenable' ) || window.MWVE_FORCE_EDIT_CHECK_ENABLED )
|
||||
};
|
||||
|
||||
require( './EditCheckContextItem.js' );
|
||||
require( './EditCheckInspector.js' );
|
||||
|
||||
mw.editcheck = {};
|
||||
|
||||
mw.editcheck.config = require( './config.json' );
|
||||
require( './EditCheckDialog.js' );
|
||||
require( './EditCheckFactory.js' );
|
||||
require( './EditCheckAction.js' );
|
||||
require( './BaseEditCheck.js' );
|
||||
|
||||
mw.editcheck.accountShouldSeeEditCheck = function ( config ) {
|
||||
// account status:
|
||||
|
@ -15,60 +20,52 @@ mw.editcheck.accountShouldSeeEditCheck = function ( config ) {
|
|||
if ( config.account === 'loggedin' && !mw.user.isNamed() ) {
|
||||
return false;
|
||||
}
|
||||
if ( config.maximumEditcount && mw.config.get( 'wgUserEditCount', 0 ) > config.maximumEditcount ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
mw.editcheck.shouldApplyToSection = function ( documentModel, selection, config ) {
|
||||
const ignoreSections = config.ignoreSections || [];
|
||||
if ( ignoreSections.length === 0 && !config.ignoreLeadSection ) {
|
||||
// Nothing is forbidden, so everything is permitted
|
||||
return true;
|
||||
}
|
||||
const isHeading = function ( nodeType ) {
|
||||
return nodeType === 'mwHeading';
|
||||
};
|
||||
// Note: we set a limit of 1 here because otherwise this will turn around
|
||||
// to keep looking when it hits the document boundary:
|
||||
const heading = documentModel.getNearestNodeMatching( isHeading, selection.getRange().start, -1, 1 );
|
||||
if ( !heading ) {
|
||||
// There's no preceding heading, so work out if we count as being in a
|
||||
// lead section. It's only a lead section if there's more headings
|
||||
// later in the document, otherwise it's just a stub article.
|
||||
return !(
|
||||
config.ignoreLeadSection &&
|
||||
!!documentModel.getNearestNodeMatching( isHeading, selection.getRange().start, 1 )
|
||||
);
|
||||
}
|
||||
if ( ignoreSections.length === 0 ) {
|
||||
// There's nothing left to deny
|
||||
return true;
|
||||
}
|
||||
const compare = new Intl.Collator( documentModel.getLang(), { sensitivity: 'accent' } ).compare;
|
||||
const headingText = documentModel.data.getText( false, heading.getRange() );
|
||||
for ( let i = ignoreSections.length - 1; i >= 0; i-- ) {
|
||||
if ( compare( headingText, ignoreSections[ i ] ) === 0 ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// TODO: Load these checks behind feature flags
|
||||
// require( './ConvertReferenceEditCheck.js' );
|
||||
// require( './TextMatchEditCheck.js' );
|
||||
if ( mw.editcheck.accountShouldSeeEditCheck( mw.editcheck.config.addReference ) || mw.editcheck.ecenable ) {
|
||||
require( './AddReferenceEditCheck.js' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find added content in the document model that might need a reference
|
||||
* Return the content ranges (content branch node interiors) contained within a range
|
||||
*
|
||||
* @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
|
||||
* 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.findAddedContentNeedingReference = function ( documentModel, includeReferencedContent ) {
|
||||
if ( mw.config.get( 'wgNamespaceNumber' ) !== mw.config.get( 'wgNamespaceIds' )[ '' ] ) {
|
||||
return [];
|
||||
}
|
||||
mw.editcheck.getContentRanges = function ( documentModel, range, covers ) {
|
||||
const ranges = [];
|
||||
documentModel.selectNodes( range, 'branches' ).forEach( ( 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.Diff = function MWEditCheckDiff( surface ) {
|
||||
this.surface = surface;
|
||||
this.documentModel = surface.getModel().getDocument();
|
||||
};
|
||||
OO.initClass( mw.editcheck.Diff );
|
||||
mw.editcheck.Diff.prototype.getModifiedRanges = function ( coveredNodesOnly ) {
|
||||
const documentModel = this.documentModel;
|
||||
if ( !documentModel.completeHistory.getLength() ) {
|
||||
return [];
|
||||
}
|
||||
|
@ -96,66 +93,13 @@ mw.editcheck.findAddedContentNeedingReference = function ( documentModel, includ
|
|||
ve.batchPush(
|
||||
ranges,
|
||||
// 2. Only fully inserted paragraphs (ranges that cover the whole node) (T345121)
|
||||
mw.editcheck.getContentRanges( documentModel, insertedRange, true )
|
||||
mw.editcheck.getContentRanges( documentModel, insertedRange, coveredNodesOnly )
|
||||
);
|
||||
}
|
||||
}
|
||||
// Reached the end of the doc / start of internal list, stop searching
|
||||
return offset < endOffset;
|
||||
} );
|
||||
const addedTextRanges = ranges.filter( ( range ) => {
|
||||
const 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 ( let 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)
|
||||
const branchNode = documentModel.getBranchNodeFromOffset( range.start );
|
||||
if ( branchNode.getParent() !== documentModel.attachedRoot ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} );
|
||||
|
||||
return addedTextRanges
|
||||
.map( ( range ) => new ve.dm.LinearSelection( range ) )
|
||||
.filter( ( selection ) => mw.editcheck.shouldApplyToSection( documentModel, selection, mw.editcheck.config.addReference ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
const ranges = [];
|
||||
documentModel.selectNodes( range, 'branches' ).forEach( ( 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;
|
||||
};
|
||||
|
||||
|
@ -208,13 +152,9 @@ if ( mw.config.get( 'wgVisualEditorConfig' ).editCheckTagging ) {
|
|||
} );
|
||||
}
|
||||
|
||||
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' ) ||
|
||||
!!window.MWVE_FORCE_EDIT_CHECK_ENABLED
|
||||
) {
|
||||
if ( mw.config.get( 'wgVisualEditorConfig' ).editCheck || mw.editcheck.ecenable ) {
|
||||
let saveProcessDeferred;
|
||||
|
||||
mw.hook( 've.preSaveProcess' ).add( ( saveProcess, target ) => {
|
||||
const surface = target.getSurface();
|
||||
|
||||
|
@ -229,9 +169,8 @@ if (
|
|||
// clear rejection-reasons between runs of the save process, so only the last one counts
|
||||
mw.editcheck.rejections.length = 0;
|
||||
|
||||
let selections = mw.editcheck.findAddedContentNeedingReference( surface.getModel().getDocument() );
|
||||
|
||||
if ( selections.length ) {
|
||||
let checks = mw.editcheck.editCheckFactory.createAllByListener( 'onBeforeSave', surface );
|
||||
if ( checks.length ) {
|
||||
mw.editcheck.refCheckShown = true;
|
||||
|
||||
const surfaceView = surface.getView();
|
||||
|
@ -272,19 +211,22 @@ if (
|
|||
saveProcessDeferred = ve.createDeferred();
|
||||
const context = surface.getContext();
|
||||
|
||||
// TODO: Allow multiple selections to be shown when multicheck is enabled
|
||||
selections = selections.slice( 0, 1 );
|
||||
// TODO: Allow multiple checks to be shown when multicheck is enabled
|
||||
checks = checks.slice( 0, 1 );
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
const drawSelections = ( selections ) => {
|
||||
const drawSelections = ( checks ) => {
|
||||
const highlightNodes = [];
|
||||
selections.forEach( ( selection ) => {
|
||||
highlightNodes.push.apply( highlightNodes, surfaceView.getDocument().selectNodes( selection.getCoveringRange(), 'branches' ).map( ( spec ) => spec.node ) );
|
||||
const selections = [];
|
||||
checks.forEach( ( check ) => {
|
||||
highlightNodes.push.apply( highlightNodes, surfaceView.getDocument().selectNodes( check.highlight.getSelection().getCoveringRange(), 'branches' ).map( ( spec ) => spec.node ) );
|
||||
const selection = ve.ce.Selection.static.newFromModel( check.highlight.getSelection(), surfaceView );
|
||||
selections.push( selection );
|
||||
} );
|
||||
// TODO: Make selections clickable when multicheck is enabled
|
||||
surfaceView.drawSelections(
|
||||
'editCheck',
|
||||
selections.map( ( selection ) => ve.ce.Selection.static.newFromModel( selection, surfaceView ) )
|
||||
checks.map( ( check ) => ve.ce.Selection.static.newFromModel( check.highlight.getSelection(), surfaceView ) )
|
||||
);
|
||||
surfaceView.setReviewMode( true, highlightNodes );
|
||||
};
|
||||
|
@ -294,7 +236,7 @@ if (
|
|||
// this is the back button
|
||||
return saveProcessDeferred.resolve();
|
||||
}
|
||||
const selectionIndex = selections.indexOf( contextData.selection );
|
||||
const selectionIndex = checks.indexOf( contextData.action );
|
||||
|
||||
if ( responseData.action !== 'reject' ) {
|
||||
mw.notify( ve.msg( 'editcheck-dialog-addref-success-notify' ), { type: 'success' } );
|
||||
|
@ -302,18 +244,18 @@ if (
|
|||
mw.editcheck.rejections.push( responseData.reason );
|
||||
}
|
||||
// TODO: Move on to the next issue, when multicheck is enabled
|
||||
// selections = mw.editcheck.findAddedContentNeedingReference( surface.getModel().getDocument() );
|
||||
selections = [];
|
||||
// checks = mw.editcheck.editCheckFactory.createAllByListener( 'onBeforeSave', surface );
|
||||
checks = [];
|
||||
|
||||
if ( selections.length ) {
|
||||
if ( checks.length ) {
|
||||
context.removePersistentSource( 'editCheckReferences' );
|
||||
setTimeout( () => {
|
||||
// timeout needed to wait out the newly added content being focused
|
||||
surface.getModel().setNullSelection();
|
||||
drawSelections( selections );
|
||||
drawSelections( checks );
|
||||
setTimeout( () => {
|
||||
// timeout needed to allow the context to reposition
|
||||
showCheckContext( selections[ Math.min( selectionIndex, selections.length - 1 ) ] );
|
||||
showCheckContext( checks[ Math.min( selectionIndex, checks.length - 1 ) ] );
|
||||
} );
|
||||
}, 500 );
|
||||
} else {
|
||||
|
@ -322,8 +264,8 @@ if (
|
|||
};
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function showCheckContext( selection ) {
|
||||
const fragment = surface.getModel().getFragment( selection, true );
|
||||
function showCheckContext( check ) {
|
||||
const fragment = check.highlight;
|
||||
|
||||
// Select the found content to correctly position the context on desktop
|
||||
fragment.select();
|
||||
|
@ -331,8 +273,8 @@ if (
|
|||
context.addPersistentSource( {
|
||||
embeddable: false,
|
||||
data: {
|
||||
action: check,
|
||||
fragment: fragment,
|
||||
selection: selection,
|
||||
callback: contextDone,
|
||||
saveProcessDeferred: saveProcessDeferred
|
||||
},
|
||||
|
@ -348,13 +290,12 @@ if (
|
|||
} );
|
||||
}
|
||||
|
||||
drawSelections( selections );
|
||||
drawSelections( checks );
|
||||
toolbar.toggle( false );
|
||||
target.onContainerScroll();
|
||||
|
||||
saveProcess.next( () => {
|
||||
|
||||
showCheckContext( selections[ 0 ] );
|
||||
showCheckContext( checks[ 0 ] );
|
||||
|
||||
return saveProcessDeferred.promise().then( ( data ) => {
|
||||
context.removePersistentSource( 'editCheckReferences' );
|
||||
|
|
|
@ -632,6 +632,13 @@
|
|||
"editcheck/modules/init.js",
|
||||
"editcheck/modules/EditCheckContextItem.js",
|
||||
"editcheck/modules/EditCheckInspector.js",
|
||||
"editcheck/modules/EditCheckDialog.js",
|
||||
"editcheck/modules/EditCheckFactory.js",
|
||||
"editcheck/modules/EditCheckAction.js",
|
||||
"editcheck/modules/BaseEditCheck.js",
|
||||
"editcheck/modules/AddReferenceEditCheck.js",
|
||||
"editcheck/modules/ConvertReferenceEditCheck.js",
|
||||
"editcheck/modules/TextMatchEditCheck.js",
|
||||
{
|
||||
"name": "editcheck/modules/config.json",
|
||||
"callback": "\\MediaWiki\\Extension\\VisualEditor\\EditCheck\\ResourceLoaderData::getConfig"
|
||||
|
|
Loading…
Reference in a new issue