EditCheck: move checks to a sidebar

Bug: T341308
Bug: T379443
Change-Id: I66147d95fc23d0f72960ff93a76b3e5ba65ce44e
This commit is contained in:
David Lynch 2024-07-22 09:55:55 -05:00 committed by DLynch
parent b0c8e92155
commit 37627ad9ae
13 changed files with 765 additions and 175 deletions

View file

@ -18,6 +18,7 @@
"editcheck-dialog-addref-success-notify": "Thank you for adding a citation!",
"editcheck-dialog-addref-title": "Add a citation",
"editcheck-dialog-title": "Before publishing",
"editcheck-review-title": "Review changes",
"tag-editcheck-reference-decline-common-knowledge": "Edit Check (references) declined (common knowledge)",
"tag-editcheck-reference-decline-common-knowledge-description": "EditCheck reference was declined as common knowledge",
"tag-editcheck-reference-decline-irrelevant": "Edit Check (references) declined (irrelevant)",

View file

@ -19,6 +19,7 @@
"editcheck-dialog-addref-success-notify": "Notification messages shown after a citation is added successfully.",
"editcheck-dialog-addref-title": "Title for the edit check context asking user to add a citation.",
"editcheck-dialog-title": "Title shown in the toolbar while the user is in the add a citation workflow.",
"editcheck-review-title": "Title shown in the sidebar / drawer while checks are displayed",
"tag-editcheck-reference-decline-common-knowledge": "Short description of the editcheck-reference-decline-common-knowledge tag.\n\nTag added when a user declines to add a suggested reference and selects the \"common knowledge\" reason.\n\nSee also:\n* {{msg-mw|editcheck-dialog-addref-reject-common-knowledge}}",
"tag-editcheck-reference-decline-common-knowledge-description": "Long description of the editcheck-reference-decline-common-knowledge tag.\n\nTag added when a user declines to add a suggested reference and selects the \"common knowledge\" reason.\n\nSee also:\n* {{msg-mw|editcheck-dialog-addref-reject-common-knowledge}}",
"tag-editcheck-reference-decline-irrelevant": "Short description of the editcheck-reference-decline-irrelevant tag.\n\nTag added when a user declines to add a suggested reference and selects the \"irrelevant\" reason.\n\nSee also:\n* {{msg-mw|editcheck-dialog-addref-reject-irrelevant}}",

View file

@ -26,8 +26,19 @@ mw.editcheck.BaseEditCheck.static.defaultConfig = {
ignoreLeadSection: false
};
mw.editcheck.BaseEditCheck.static.title = ve.msg( 'editcheck-review-title' );
mw.editcheck.BaseEditCheck.static.description = ve.msg( 'editcheck-dialog-addref-description' );
/**
* Get the name of the check type
*
* @return {string} Check type name
*/
mw.editcheck.BaseEditCheck.prototype.getName = function () {
return this.constructor.static.name;
};
/**
* @param {ve.dm.Surface} surfaceModel
* @return {mw.editcheck.EditCheckAction[]}
@ -43,7 +54,8 @@ mw.editcheck.BaseEditCheck.prototype.onDocumentChange = null;
/**
* @param {string} choice `action` key from static.choices
* @param {mw.editcheck.EditCheckAction} action
* @param {ve.ui.EditCheckContextItem} contextItem
* @param {ve.ui.Surface} surface
* @return {jQuery.Promise} Promise which resolves when action is complete
*/
mw.editcheck.BaseEditCheck.prototype.act = null;
@ -55,6 +67,16 @@ mw.editcheck.BaseEditCheck.prototype.getChoices = function () {
return this.constructor.static.choices;
};
/**
* Get the title of the check
*
* @param {mw.editcheck.EditCheckAction} action
* @return {jQuery|string|Function|OO.ui.HtmlSnippet}
*/
mw.editcheck.BaseEditCheck.prototype.getTitle = function () {
return this.constructor.static.title;
};
/**
* @param {mw.editcheck.EditCheckAction} action
* @return {string}

View file

@ -1,8 +1,29 @@
@import '../../lib/codex-design-tokens/theme-wikimedia-ui.less';
@media ( max-width: 1492px ) {
/* Hides the Vector sidebar while an editcheck-enabled editing session is occurring. See T379443. */
/* stylelint-disable selector-class-pattern */
.ve-editcheck-available {
.mw-body .vector-column-end,
.vector-pinnable-header-pin-button {
display: none !important; /* stylelint-disable-line declaration-no-important */
}
}
/* stylelint-enable selector-class-pattern */
}
/* Toolbar */
.ve-ui-editCheck-toolbar {
font-size: 0.875rem; // ignore content scaling
.mw-mf & {
font-size: inherit;
}
.oo-ui-toolbar-bar {
display: flex;
flex-wrap: wrap;
}
.oo-ui-toolbar-tools {
@ -14,6 +35,12 @@
flex: 0;
}
.oo-ui-toolbar-actions + div {
// There's a clear div that we need to enhance slightly
flex-basis: 100%;
height: 0;
}
&.ve-init-mw-mobileArticleTarget-toolbar .oo-ui-toolbar-tools.oo-ui-toolbar-after {
display: none;
}
@ -44,6 +71,162 @@
border-bottom-color: #fce7fe;
}
/* Actions */
.ve-ui-editCheckDialog {
font-size: 0.875rem; // ignore content scaling
.oo-ui-window-body {
padding-left: 12px;
}
&-title {
display: block;
font-weight: bold;
padding: @spacing-50 0;
margin-bottom: @spacing-50;
border-bottom: 1px solid @border-color-base;
}
&-footer {
display: flex;
justify-content: flex-end;
> .oo-ui-labelWidget {
align-content: center;
}
}
.ve-ui-editCheckActionWidget {
box-sizing: border-box;
border: @border-base;
margin: @spacing-100 0;
white-space: normal; // Minerva needs this
&-head {
position: relative;
padding: @spacing-50 @spacing-75;
> .oo-ui-labelElement-label {
display: block;
margin-left: 2em;
font-weight: @font-weight-semi-bold;
}
}
&-body {
margin: @spacing-50 @spacing-75 @spacing-100;
}
&-actions {
margin-top: @spacing-100;
}
&.ve-ui-editCheckActionWidget-collapsed {
filter: grayscale( 1 );
> .ve-ui-editCheckActionWidget-body {
display: none;
}
}
&.oo-ui-flaggedElement-warning {
border-color: @border-color-warning;
> .ve-ui-editCheckActionWidget-head {
background-color: @background-color-warning-subtle;
}
}
&.oo-ui-flaggedElement-error {
border-color: @border-color-error;
> .ve-ui-editCheckActionWidget-head {
background-color: @background-color-error-subtle;
}
}
&.oo-ui-flaggedElement-success {
border-color: @border-color-success;
> .ve-ui-editCheckActionWidget-head {
background-color: @background-color-success-subtle;
}
}
&.oo-ui-flaggedElement-notice {
border-color: @border-color-notice;
> .ve-ui-editCheckActionWidget-head {
background-color: @background-color-notice-subtle;
}
}
}
&.ve-ui-editCheckDialog-singleAction .ve-ui-editCheckActionWidget-collapsed {
display: none;
}
.mw-mf & {
top: auto;
// See: .ve-ui-mobileContext, which this is closely mimicking
background-color: @background-color-interactive-subtle;
/* Match toolbar border & shadow */
border-top: @border-subtle;
box-shadow: 0 -1px 1px 0 rgba( 0, 0, 0, 0.1 );
/* Transition out faster, as keyboard may be coming up */
transition: transform 100ms;
transform: translateY( 0% );
max-width: 995px;
margin: 0 auto;
&-title {
padding: @spacing-75 @spacing-100;
margin-bottom: 0;
}
&-close {
position: absolute;
right: 0;
top: 2px;
}
&-footer {
margin: 0 @spacing-100 @spacing-50;
}
&.ve-ui-editCheckDialog-collapsed {
display: block;
transition: transform 250ms;
transform: translateY( calc( 100% - 2.5em ) );
}
.oo-ui-window-body {
padding: 0;
}
.ve-ui-editCheckActionWidget {
margin: 0;
border-width: 0;
&-head {
background-color: transparent !important; /* stylelint-disable-line declaration-no-important */
}
&-body {
padding-left: 2em;
margin-bottom: @spacing-75;
}
}
}
}
.mw-mf .ve-ce-surface-reviewMode.ve-ce-surface-deactivated {
// Otherwise the content will be covered by the mobile context at the end
// of the document. (Upstream this?)
margin-bottom: 100%;
}
/* Selections */
.ve-ce-surface-reviewMode + .ve-ui-overlay .ve-ce-surface-selections-editCheck .ve-ce-surface-selection {
@ -68,9 +251,41 @@
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: 1px solid ( ( #a66200 - 0.8 * ( #fff ) ) / 0.2 );
border-radius: 2px;
padding: 2px;
margin: -2px 0 0 -2px;
}
}
.ve-ui-editCheck-gutter-highlight {
position: absolute;
left: 0;
width: 2px;
overflow: hidden;
background-color: @color-base;
&-error {
background-color: @color-error;
}
&-warning {
background-color: @color-warning;
}
&-notice {
background-color: @color-notice;
}
&-success {
background-color: @color-success;
}
&-inactive {
background-color: @border-color-base;
}
.mw-mf & {
left: -10px;
}
}

View file

@ -5,15 +5,28 @@
* @param {mw.editcheck.BaseEditCheck} check
* @param {ve.dm.SurfaceFragment[]} fragments Affected fragments
* @param {jQuery|string|Function|OO.ui.HtmlSnippet} message Check message body
* @param {jQuery|string|Function|OO.ui.HtmlSnippet} title Check title
*/
mw.editcheck.EditCheckAction = function MWEditCheckAction( config ) {
this.check = config.check;
this.fragments = config.fragments;
this.message = config.message;
this.title = config.title;
this.icon = config.icon;
this.type = config.type || 'warning';
};
OO.initClass( mw.editcheck.EditCheckAction );
/**
* Get the action's title
*
* @return {jQuery|string|Function|OO.ui.HtmlSnippet}
*/
mw.editcheck.EditCheckAction.prototype.getTitle = function () {
return this.title || this.check.getTitle( this );
};
/**
* Get the available choices
*
@ -38,5 +51,137 @@ mw.editcheck.EditCheckAction.prototype.getHighlightSelections = function () {
* @return {string}
*/
mw.editcheck.EditCheckAction.prototype.getDescription = function () {
return this.check.getDescription( this );
return this.message || this.check.getDescription( this );
};
mw.editcheck.EditCheckAction.prototype.getType = function () {
return this.type;
};
/**
* Get the name of the check type
*
* @return {string} Check type name
*/
mw.editcheck.EditCheckAction.prototype.getName = function () {
return this.check.getName();
};
mw.editcheck.EditCheckAction.prototype.render = function ( collapsed, singleAction, surface ) {
const widget = new mw.editcheck.EditCheckActionWidget( {
type: this.type,
icon: this.icon,
label: this.getTitle(),
message: this.getDescription(),
classes: collapsed ? [ 've-ui-editCheckActionWidget-collapsed' ] : '',
singleAction: singleAction
} );
this.getChoices().forEach( ( choice ) => {
const button = new OO.ui.ButtonWidget( choice );
button.connect( this, {
click: () => {
const promise = this.check.act( choice.action, this, surface ) || ve.createDeferred().resolve().promise();
widget.emit( 'act', choice, choice.action, promise );
}
} );
widget.addAction( button );
} );
return widget;
};
mw.editcheck.EditCheckActionWidget = function MWEditCheckActionWidget( config ) {
// Configuration initialization
config = config || {};
this.singleAction = config.singleAction;
this.actions = [];
// Parent constructor
mw.editcheck.EditCheckActionWidget.super.call( this, config );
// Mixin constructors
OO.ui.mixin.IconElement.call( this, config );
OO.ui.mixin.LabelElement.call( this, config );
OO.ui.mixin.TitledElement.call( this, config );
OO.ui.mixin.FlaggedElement.call( this, config );
this.setType( config.type );
if ( config.icon ) {
this.setIcon( config.icon );
}
this.message = new OO.ui.LabelWidget( { label: config.message } );
this.$actions = $( '<div>' ).addClass( 've-ui-editCheckActionWidget-actions oo-ui-element-hidden' );
this.$head = $( '<div>' )
.append( this.$icon, this.$label )
.addClass( 've-ui-editCheckActionWidget-head' )
.on( 'click', this.onHeadClick.bind( this ) );
this.$body = $( '<div>' )
.append( this.message.$element, this.$actions )
.addClass( 've-ui-editCheckActionWidget-body' );
this.$element
.append( this.$head, this.$body )
// .append( this.$icon, this.$label, this.closeButton && this.closeButton.$element )
.addClass( 've-ui-editCheckActionWidget' );
};
OO.inheritClass( mw.editcheck.EditCheckActionWidget, OO.ui.Widget );
OO.mixinClass( mw.editcheck.EditCheckActionWidget, OO.ui.mixin.IconElement );
OO.mixinClass( mw.editcheck.EditCheckActionWidget, OO.ui.mixin.LabelElement );
OO.mixinClass( mw.editcheck.EditCheckActionWidget, OO.ui.mixin.TitledElement );
OO.mixinClass( mw.editcheck.EditCheckActionWidget, OO.ui.mixin.FlaggedElement );
/**
* @inheritdoc
*/
mw.editcheck.EditCheckActionWidget.prototype.setDisabled = function ( disabled ) {
OO.ui.Widget.prototype.setDisabled.call( this, disabled );
this.actions.forEach( ( action ) => {
action.setDisabled( disabled );
} );
};
mw.editcheck.EditCheckActionWidget.static.iconMap = {
notice: 'infoFilled',
error: 'error',
warning: 'alert'
};
mw.editcheck.EditCheckActionWidget.prototype.setType = function ( type ) {
if ( !this.constructor.static.iconMap[ type ] ) {
type = 'notice';
}
if ( type !== this.type ) {
this.clearFlags();
this.setFlags( type );
this.setIcon( this.constructor.static.iconMap[ type ] );
}
this.type = type;
};
mw.editcheck.EditCheckActionWidget.prototype.getType = function () {
return this.type;
};
mw.editcheck.EditCheckActionWidget.prototype.addAction = function ( action ) {
this.actions.push( action );
this.$actions.append( action.$element ).removeClass( 'oo-ui-element-hidden' );
};
mw.editcheck.EditCheckActionWidget.prototype.onHeadClick = function ( e ) {
if ( this.singleAction ) {
return;
}
e.preventDefault();
// eslint-disable-next-line no-jquery/no-class-state
this.$element.toggleClass( 've-ui-editCheckActionWidget-collapsed' );
// eslint-disable-next-line no-jquery/no-class-state
this.emit( 'togglecollapse', this.$element.hasClass( 've-ui-editCheckActionWidget-collapsed' ) );
};

View file

@ -27,15 +27,14 @@ OO.inheritClass( ve.ui.EditCheckDialog, ve.ui.ToolbarDialog );
ve.ui.EditCheckDialog.static.name = 'editCheckDialog';
ve.ui.EditCheckDialog.static.position = 'side';
ve.ui.EditCheckDialog.static.position = OO.ui.isMobile() ? 'below' : 'side';
ve.ui.EditCheckDialog.static.size = 'medium';
ve.ui.EditCheckDialog.static.size = OO.ui.isMobile() ? 'full' : 'medium';
ve.ui.EditCheckDialog.static.framed = false;
// // Invisible title for accessibility
// ve.ui.EditCheckDialog.static.title =
// OO.ui.deferMsg( 'visualeditor-find-and-replace-title' );
// Invisible title for accessibility
ve.ui.EditCheckDialog.static.title = OO.ui.deferMsg( 'editcheck-review-title' );
/* Methods */
@ -46,26 +45,190 @@ ve.ui.EditCheckDialog.prototype.initialize = function () {
// Parent method
ve.ui.EditCheckDialog.super.prototype.initialize.call( this );
this.title = new OO.ui.LabelWidget( {
label: this.constructor.static.title,
classes: [ 've-ui-editCheckDialog-title' ]
} );
// FIXME: click handlers are getting unbound when the window is closed
this.closeButton = new OO.ui.ButtonWidget( {
classes: [ 've-ui-editCheckDialog-close' ],
framed: false,
label: ve.msg( 'visualeditor-contextitemwidget-label-close' ),
invisibleLabel: true,
icon: 'expand'
} ).connect( this, {
click: 'onCloseButtonClick'
} );
this.currentOffset = 0;
this.footerLabel = new OO.ui.LabelWidget();
this.previousButton = new OO.ui.ButtonWidget( {
icon: 'previous',
title: ve.msg( 'last' ),
invisibleLabel: true,
framed: false
} ).connect( this, {
click: 'onPreviousButtonClick'
} );
this.nextButton = new OO.ui.ButtonWidget( {
icon: 'next',
title: ve.msg( 'next' ),
invisibleLabel: true,
framed: false
} ).connect( this, {
click: 'onNextButtonClick'
} );
this.footer = new OO.ui.HorizontalLayout( {
classes: [ 've-ui-editCheckDialog-footer' ],
items: [
this.footerLabel,
this.previousButton,
this.nextButton
]
} );
this.$checks = $( '<div>' );
this.$body.append( this.title.$element, this.closeButton.$element, this.$checks, this.footer.$element );
this.$highlights = $( '<div>' );
this.updateDebounced = ve.debounce( this.update.bind( this ), 100 );
this.positionDebounced = ve.debounce( this.position.bind( this ), 100 );
};
ve.ui.EditCheckDialog.prototype.update = function () {
const surfaceView = this.surface.getView();
const checks = mw.editcheck.editCheckFactory.createAllByListener( 'onDocumentChange', this.surface.getModel() );
const $checks = $( '<div>' );
const selections = [];
checks.forEach( ( check ) => {
$checks.append( new OO.ui.MessageWidget( {
type: 'warning',
label: check.message,
framed: false
} ).$element );
// We only regenerate the checks on-change during the edit. If we're in
// the proofreading step, no new checks should appear based on changes:
if ( this.listener === 'onDocumentChange' || !this.currentChecks ) {
this.currentChecks = mw.editcheck.editCheckFactory.createAllByListener( this.listener, this.surface.getModel() );
}
if ( this.listener === 'onBeforeSave' && this.currentChecks.length === 0 ) {
return this.close( 'complete' );
}
const checks = this.currentChecks;
const newOffset = Math.min( this.currentOffset, checks.length - 1 );
this.$checks.empty();
this.$highlights.empty();
checks.forEach( ( check, index ) => {
const widget = check.render( index !== newOffset, this.listener === 'onBeforeSave', this.surface );
widget.on( 'togglecollapse', this.onToggleCollapse, [ check, index ], this );
widget.on( 'act', this.onAct, [ widget ], this );
this.$checks.append( widget.$element );
check.widget = widget;
} );
if ( this.reviewMode ) {
// Review mode grays out everything that's not highlighted:
const highlightNodes = [];
checks.forEach( ( check ) => {
check.getHighlightSelections().forEach( ( selection ) => {
highlightNodes.push.apply( highlightNodes, surfaceView.getDocument().selectNodes( selection.getCoveringRange(), 'branches' ).map( ( spec ) => spec.node ) );
} );
} );
surfaceView.setReviewMode( true, highlightNodes );
}
this.setCurrentOffset( newOffset );
};
ve.ui.EditCheckDialog.prototype.position = function () {
this.drawHighlights();
this.scrollCurrentCheckIntoView();
};
ve.ui.EditCheckDialog.prototype.drawHighlights = function () {
const surfaceView = this.surface.getView();
this.$highlights.empty();
this.currentChecks.forEach( ( check, index ) => {
check.getHighlightSelections().forEach( ( selection ) => {
selections.push( ve.ce.Selection.static.newFromModel( selection, surfaceView ) );
const selectionView = ve.ce.Selection.static.newFromModel( selection, surfaceView );
const rect = selectionView.getSelectionBoundingRect();
// The following classes are used here:
// * ve-ui-editCheck-gutter-highlight-error
// * ve-ui-editCheck-gutter-highlight-warning
// * ve-ui-editCheck-gutter-highlight-notice
// * ve-ui-editCheck-gutter-highlight-success
// * ve-ui-editCheck-gutter-highlight-active
// * ve-ui-editCheck-gutter-highlight-inactive
this.$highlights.append( $( '<div>' )
.addClass( 've-ui-editCheck-gutter-highlight' )
.addClass( 've-ui-editCheck-gutter-highlight-' + check.getType() )
.addClass( 've-ui-editCheck-gutter-highlight-' + ( index === this.currentOffset ? 'active' : 'inactive' ) )
.css( {
top: rect.top - 2,
height: rect.height + 4
} )
);
} );
} );
surfaceView.getSelectionManager().drawSelections( 'editCheckWarning', selections );
this.$body.empty().append( $checks );
surfaceView.appendHighlights( this.$highlights, false );
};
/**
* Set the offset of the current check, within the list of all checks
*
* @param {number} offset
*/
ve.ui.EditCheckDialog.prototype.setCurrentOffset = function ( offset ) {
// TODO: work out how to tell the window to recalculate height here
this.currentOffset = Math.max( 0, offset );
this.$body.find( '.ve-ui-editCheckActionWidget' ).each( ( i, el ) => {
$( el ).toggleClass( 've-ui-editCheckActionWidget-collapsed', i !== this.currentOffset );
} );
this.footerLabel.setLabel(
ve.msg( 'visualeditor-find-and-replace-results',
ve.init.platform.formatNumber( this.currentOffset + 1 ),
ve.init.platform.formatNumber( this.currentChecks.length )
)
);
this.nextButton.setDisabled( this.currentOffset >= this.currentChecks.length - 1 );
this.previousButton.setDisabled( this.currentOffset <= 0 );
this.updateSize();
const surfaceView = this.surface.getView();
if ( this.currentChecks.length > 0 ) {
// The currently-focused check gets a selection:
// TODO: clicking the selection should activate the sidebar-action
surfaceView.getSelectionManager().drawSelections(
'editCheckWarning',
this.currentChecks[ this.currentOffset ].getHighlightSelections().map(
( selection ) => ve.ce.Selection.static.newFromModel( selection, surfaceView )
)
);
this.scrollCurrentCheckIntoView();
} else {
surfaceView.getSelectionManager().drawSelections( 'editCheckWarning', [] );
}
this.drawHighlights();
};
ve.ui.EditCheckDialog.prototype.scrollCurrentCheckIntoView = function () {
const currentCheck = this.currentChecks[ this.currentOffset ];
if ( currentCheck ) {
// scrollSelectionIntoView scrolls to the focus of a selection, but we
// want the very beginning to be in view, so collapse it:
const selection = currentCheck.getHighlightSelections()[ 0 ].collapseToStart();
this.surface.scrollSelectionIntoView( selection, {
animate: true,
padding: {
top: ( OO.ui.isMobile() ? 80 : currentCheck.widget.$element[ 0 ].getBoundingClientRect().top ),
bottom: ( OO.ui.isMobile() ? this.getContentHeight() : 0 ) + 20
},
alignToTop: true
} );
}
};
/**
@ -74,8 +237,24 @@ ve.ui.EditCheckDialog.prototype.update = function () {
ve.ui.EditCheckDialog.prototype.getSetupProcess = function ( data ) {
return ve.ui.EditCheckDialog.super.prototype.getSetupProcess.call( this, data )
.first( () => {
this.currentOffset = 0;
this.listener = data.listener || 'onDocumentChange';
this.reviewMode = data.reviewMode;
this.surface = data.surface;
this.surface.getModel().on( 'undoStackChange', this.updateDebounced );
this.surface.getView().on( 'position', this.positionDebounced );
this.closeButton.toggle( OO.ui.isMobile() );
this.footer.toggle(
this.listener === 'onBeforeSave' &&
!mw.config.get( 'wgVisualEditorConfig' ).editCheckSingle
);
this.$element.toggleClass( 've-ui-editCheckDialog-singleAction', this.listener === 'onBeforeSave' );
this.surface.context.hide();
this.update();
}, this );
};
@ -86,6 +265,12 @@ ve.ui.EditCheckDialog.prototype.getSetupProcess = function ( data ) {
ve.ui.EditCheckDialog.prototype.getReadyProcess = function ( data ) {
return ve.ui.EditCheckDialog.super.prototype.getReadyProcess.call( this, data )
.next( () => {
// The end of the ready process triggers a reflow after an
// animation, so we need to get past that to avoid the content
// being immediately scrolled away
setTimeout( () => {
this.scrollCurrentCheckIntoView();
}, 500 );
}, this );
};
@ -95,17 +280,105 @@ ve.ui.EditCheckDialog.prototype.getReadyProcess = function ( data ) {
ve.ui.EditCheckDialog.prototype.getTeardownProcess = function ( data ) {
return ve.ui.EditCheckDialog.super.prototype.getTeardownProcess.call( this, data )
.next( () => {
this.surface.getView().setReviewMode( false );
this.surface.getView().getSelectionManager().drawSelections( 'editCheckWarning', [] );
this.surface.getView().off( 'position', this.positionDebounced );
this.surface.getModel().off( 'undoStackChange', this.updateDebounced );
this.$highlights.remove().empty();
this.$checks.empty();
}, this );
};
/**
* Handle 'act' events from the mw.widget.EditCheckActionWidget
*
* @param {mw.editcheck.EditCheckActionWidget} widget
* @param {Object} choice Choice object (with 'reason', 'object', 'label')
* @param {string} actionChosen Choice action
* @param {jQuery.Promise} promise Promise which resolves when the action is complete
*/
ve.ui.EditCheckDialog.prototype.onAct = function ( widget, choice, actionChosen, promise ) {
widget.setDisabled( true );
this.nextButton.setDisabled( true );
this.previousButton.setDisabled( true );
promise.then( ( data ) => {
widget.setDisabled( false );
this.nextButton.setDisabled( false );
this.previousButton.setDisabled( false );
this.surface.getModel().setNullSelection();
if ( OO.ui.isMobile() ) {
// Delay on mobile means we need to rehide this
setTimeout( () => this.surface.getModel().setNullSelection(), 300 );
}
if ( !data ) {
// Nothing happened, just fall back and leave the check
return;
}
if ( this.listener === 'onBeforeSave' ) {
// We must have been acting on the currentOffset
setTimeout( () => {
// We want to linger for a brief moment before moving away
this.currentChecks.splice( this.currentOffset, 1 );
this.currentOffset = Math.max( 0, this.currentOffset - 1 );
this.update();
}, 500 );
}
} );
};
/**
* Handle 'togglecollapse' events from the mw.widget.EditCheckActionWidget
*
* @param {mw.editcheck.EditCheckAction} check
* @param {number} index
* @param {boolean} collapsed
*/
ve.ui.EditCheckDialog.prototype.onToggleCollapse = function ( check, index, collapsed ) {
if ( !collapsed ) {
// expanded one
this.setCurrentOffset( this.currentChecks.indexOf( check ) );
}
};
/**
* Handle click events from the close button
*/
ve.ui.EditCheckDialog.prototype.onCloseButtonClick = function () {
// eslint-disable-next-line no-jquery/no-class-state
const collapse = !this.$element.hasClass( 've-ui-editCheckDialog-collapsed' );
this.$element.toggleClass( 've-ui-editCheckDialog-collapsed', collapse );
this.closeButton.setIcon( collapse ? 'collapse' : 'expand' );
};
/**
* Handle click events from the next button
*/
ve.ui.EditCheckDialog.prototype.onNextButtonClick = function () {
this.setCurrentOffset( this.currentOffset + 1 );
};
/**
* Handle click events from the previous button
*/
ve.ui.EditCheckDialog.prototype.onPreviousButtonClick = function () {
this.setCurrentOffset( this.currentOffset - 1 );
};
/* Registration */
ve.ui.windowFactory.register( ve.ui.EditCheckDialog );
ve.ui.commandRegistry.register(
new ve.ui.Command(
'editCheckDialog', 'window', 'toggle', { args: [ 'editCheckDialog' ] }
'editCheckDialogInProcess', 'window', 'toggle', { args: [ 'editCheckDialog', { listener: 'onDocumentChange' } ] }
)
);
ve.ui.commandRegistry.register(
new ve.ui.Command(
'editCheckDialogBeforeSave', 'window', 'toggle', { args: [ 'editCheckDialog', { listener: 'onBeforeSave', reviewMode: true } ] }
)
);
@ -125,7 +398,8 @@ 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';
ve.ui.EditCheckDialogTool.static.commandName = 'editCheckDialogInProcess';
// ve.ui.EditCheckDialogTool.static.commandName = 'editCheckDialogBeforeSave';
// Demo button for opening edit check sidebar
// ve.ui.toolFactory.register( ve.ui.EditCheckDialogTool );

View file

@ -66,7 +66,7 @@ mw.editcheck.EditCheckFactory.prototype.getNamesByListener = function ( listener
* @return {mw.editcheck.EditCheckActions[]} Actions, sorted by range
*/
mw.editcheck.EditCheckFactory.prototype.createAllByListener = function ( listener, surfaceModel ) {
const newChecks = [];
let newChecks = [];
this.getNamesByListener( listener ).forEach( ( checkName ) => {
const check = this.create( checkName, mw.editcheck.config[ checkName ] );
if ( !check.canBeShown() ) {
@ -80,6 +80,10 @@ mw.editcheck.EditCheckFactory.prototype.createAllByListener = function ( listene
newChecks.sort(
( a, b ) => a.getHighlightSelections()[ 0 ].getCoveringRange().start - b.getHighlightSelections()[ 0 ].getCoveringRange().start
);
if ( mw.config.get( 'wgVisualEditorConfig' ).editCheckSingle && listener === 'onBeforeSave' ) {
newChecks = newChecks.filter( ( action ) => action.getName() === 'addReference' );
newChecks.splice( 1 );
}
return newChecks;
};

View file

@ -119,7 +119,6 @@ ve.ui.EditCheckInspector.prototype.getSetupProcess = function ( data ) {
return ve.ui.EditCheckInspector.super.prototype.getSetupProcess.call( this, data )
.first( function () {
this.surface = data.surface;
this.saveProcessDeferred = data.saveProcessDeferred;
this.answerRadioSelect.selectItem( null );
}, this );
};

View file

@ -7,6 +7,8 @@ OO.inheritClass( mw.editcheck.AddReferenceEditCheck, mw.editcheck.BaseEditCheck
mw.editcheck.AddReferenceEditCheck.static.name = 'addReference';
mw.editcheck.AddReferenceEditCheck.static.title = ve.msg( 'editcheck-dialog-addref-title' );
mw.editcheck.AddReferenceEditCheck.static.description = ve.msg( 'editcheck-dialog-addref-description' );
mw.editcheck.AddReferenceEditCheck.static.defaultConfig = ve.extendObject( {}, mw.editcheck.BaseEditCheck.static.defaultConfig, {
@ -20,9 +22,11 @@ mw.editcheck.AddReferenceEditCheck.prototype.onBeforeSave = function ( surfaceMo
return new mw.editcheck.EditCheckAction( {
fragments: [ fragment ],
check: this
// icon: 'quotes',
} );
} );
};
mw.editcheck.AddReferenceEditCheck.prototype.onDocumentChange = mw.editcheck.AddReferenceEditCheck.prototype.onBeforeSave;
/**
* Find content ranges which have been inserted
@ -53,9 +57,9 @@ mw.editcheck.AddReferenceEditCheck.prototype.findAddedContent = function ( docum
return ranges;
};
mw.editcheck.AddReferenceEditCheck.prototype.act = function ( choice, action, contextItem ) {
mw.editcheck.AddReferenceEditCheck.prototype.act = function ( choice, action, surface ) {
// 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' );
const windowAction = ve.ui.actionFactory.create( 'window', surface, 'check' );
switch ( choice ) {
case 'accept':
ve.track( 'activity.editCheckReferences', { action: 'edit-check-confirm' } );
@ -67,7 +71,7 @@ mw.editcheck.AddReferenceEditCheck.prototype.act = function ( choice, action, co
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 ) => {
surface.getDialogs().once( 'closing', ( win, closed, citeData ) => {
citoidOrCiteDataDeferred.resolve( citeData );
} );
} else {
@ -75,18 +79,11 @@ mw.editcheck.AddReferenceEditCheck.prototype.act = function ( choice, action, co
// 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 {
return citoidOrCiteDataDeferred.promise().done( ( data ) => {
if ( data ) {
// Edit check inspector is already closed by this point, but
// we need to end the workflow.
contextItem.close( citoidData );
mw.notify( ve.msg( 'editcheck-dialog-addref-success-notify' ), { type: 'success' } );
}
} );
} );
@ -95,20 +92,14 @@ mw.editcheck.AddReferenceEditCheck.prototype.act = function ( choice, action, co
return windowAction.open(
'editCheckReferencesInspector',
{
fragment: action.fragments[ 0 ],
callback: contextItem.data.callback,
saveProcessDeferred: contextItem.data.saveProcessDeferred
fragment: action.fragments[ 0 ]
}
// 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 );
} ).done( ( data ) => {
if ( data && data.action === 'reject' && data.reason ) {
mw.editcheck.rejections.push( data.reason );
}
} );
}

View file

@ -7,10 +7,13 @@ OO.inheritClass( mw.editcheck.TextMatchEditCheck, mw.editcheck.BaseEditCheck );
mw.editcheck.TextMatchEditCheck.static.name = 'textMatch';
mw.editcheck.TextMatchEditCheck.static.choices = [];
mw.editcheck.TextMatchEditCheck.static.replacers = [
// TODO: Load text replacement rules from community config
{
query: 'unfortunately',
title: 'Adverb usage',
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>.' )
}
];
@ -23,6 +26,7 @@ mw.editcheck.TextMatchEditCheck.prototype.onDocumentChange = function ( surfaceM
actions.push(
new mw.editcheck.EditCheckAction( {
fragments: [ fragment ],
title: replacer.title,
message: replacer.message,
check: this
} )
@ -32,4 +36,6 @@ mw.editcheck.TextMatchEditCheck.prototype.onDocumentChange = function ( surfaceM
return actions;
};
// mw.editcheck.TextMatchEditCheck.prototype.onBeforeSave = mw.editcheck.TextMatchEditCheck.prototype.onDocumentChange;
mw.editcheck.editCheckFactory.register( mw.editcheck.TextMatchEditCheck );

View file

@ -83,8 +83,12 @@ if ( mw.config.get( 'wgVisualEditorConfig' ).editCheckTagging ) {
}
if ( mw.config.get( 'wgVisualEditorConfig' ).editCheck || mw.editcheck.ecenable ) {
let saveProcessDeferred;
mw.hook( 've.activationStart' ).add( () => {
document.documentElement.classList.add( 've-editcheck-available' );
} );
mw.hook( 've.deactivationComplete' ).add( () => {
document.documentElement.classList.remove( 've-editcheck-available' );
} );
mw.hook( 've.preSaveProcess' ).add( ( saveProcess, target ) => {
const surface = target.getSurface();
@ -101,12 +105,11 @@ if ( mw.config.get( 'wgVisualEditorConfig' ).editCheck || mw.editcheck.ecenable
// clear rejection-reasons between runs of the save process, so only the last one counts
mw.editcheck.rejections.length = 0;
let checks = mw.editcheck.editCheckFactory.createAllByListener( 'onBeforeSave', surface.getModel() );
const checks = mw.editcheck.editCheckFactory.createAllByListener( 'onBeforeSave', surface.getModel() );
if ( checks.length ) {
ve.track( 'counter.editcheck.preSaveChecksShown' );
mw.editcheck.refCheckShown = true;
const surfaceView = surface.getView();
const toolbar = target.getToolbar();
const reviewToolbar = new ve.ui.PositionedTargetToolbar( target, target.toolbarConfig );
reviewToolbar.setup( [
@ -142,122 +145,51 @@ if ( mw.config.get( 'wgVisualEditorConfig' ).editCheck || mw.editcheck.ecenable
target.toolbar.$element.before( reviewToolbar.$element );
target.toolbar = reviewToolbar;
saveProcessDeferred = ve.createDeferred();
const context = surface.getContext();
// TODO: Allow multiple checks to be shown when multicheck is enabled
checks = checks.slice( 0, 1 );
// eslint-disable-next-line no-shadow
const drawSelections = ( checks ) => {
const highlightNodes = [];
const selections = [];
checks.forEach( ( check ) => {
check.getHighlightSelections().forEach( ( selection ) => {
highlightNodes.push.apply( highlightNodes, surfaceView.getDocument().selectNodes( selection.getCoveringRange(), 'branches' ).map( ( spec ) => spec.node ) );
const selectionView = ve.ce.Selection.static.newFromModel( selection, surfaceView );
selections.push( selectionView );
} );
} );
// TODO: Make selections clickable when multicheck is enabled
surfaceView.getSelectionManager().drawSelections(
'editCheck',
selections
);
surfaceView.setReviewMode( true, highlightNodes );
};
const contextDone = ( responseData, contextData ) => {
if ( !responseData ) {
// this is the back button
return saveProcessDeferred.resolve();
}
const selectionIndex = checks.indexOf( contextData.action );
if ( responseData.action !== 'reject' ) {
mw.notify( ve.msg( 'editcheck-dialog-addref-success-notify' ), { type: 'success' } );
} else if ( responseData.reason ) {
mw.editcheck.rejections.push( responseData.reason );
}
// TODO: Move on to the next issue, when multicheck is enabled
// checks = mw.editcheck.editCheckFactory.createAllByListener( 'onBeforeSave', surface.getModel() );
checks = [];
if ( checks.length ) {
context.removePersistentSource( 'editCheckReferences' );
setTimeout( () => {
// timeout needed to wait out the newly added content being focused
surface.getModel().setNullSelection();
drawSelections( checks );
setTimeout( () => {
// timeout needed to allow the context to reposition
showCheckContext( checks[ Math.min( selectionIndex, checks.length - 1 ) ] );
} );
}, 500 );
} else {
saveProcessDeferred.resolve( true );
}
};
// eslint-disable-next-line no-inner-declarations
function showCheckContext( check ) {
const fragment = check.fragments[ 0 ];
// Select the found content to correctly position the context on desktop
fragment.select();
context.addPersistentSource( {
embeddable: false,
data: {
action: check,
fragment: fragment,
callback: contextDone,
saveProcessDeferred: saveProcessDeferred
},
name: 'editCheckReferences'
} );
// Deactivate to prevent selection suppressing mobile context
surface.getView().deactivate();
// Once the context is positioned, clear the selection
setTimeout( () => {
surface.getModel().setNullSelection();
} );
let $contextContainer, contextPadding;
if ( surface.context.popup ) {
contextPadding = surface.context.popup.containerPadding;
$contextContainer = surface.context.popup.$container;
surface.context.popup.$container = surface.$element;
surface.context.popup.containerPadding = 20;
}
drawSelections( checks );
toolbar.toggle( false );
target.onContainerScroll();
saveProcess.next( () => {
showCheckContext( checks[ 0 ] );
toolbar.toggle( false );
target.onContainerScroll();
// surface.executeCommand( 'editCheckDialogBeforeSave' );
const windowAction = ve.ui.actionFactory.create( 'window', surface, 'check' );
return windowAction.open( 'editCheckDialog', { listener: 'onBeforeSave', reviewMode: true } )
.then( ( instance ) => instance.closing )
.then( ( data ) => {
reviewToolbar.$element.remove();
toolbar.toggle( true );
target.toolbar = toolbar;
if ( $contextContainer ) {
surface.context.popup.$container = $contextContainer;
surface.context.popup.containerPadding = contextPadding;
}
// Creating a new PositionedTargetToolbar stole the
// toolbar windowmanagers, so we need to make the
// original toolbar reclaim them:
toolbar.disconnect( target );
target.setupToolbar( surface );
target.onContainerScroll();
return saveProcessDeferred.promise().then( ( data ) => {
context.removePersistentSource( 'editCheckReferences' );
surfaceView.getSelectionManager().drawSelections( 'editCheck', [] );
surfaceView.setReviewMode( false );
reviewToolbar.$element.remove();
toolbar.toggle( true );
target.toolbar = toolbar;
target.onContainerScroll();
// Check the user inserted a citation
if ( data ) {
const delay = ve.createDeferred();
// If they inserted, wait 2 seconds on desktop before showing save dialog
setTimeout( () => {
ve.track( 'counter.editcheck.preSaveChecksCompleted' );
delay.resolve();
}, !OO.ui.isMobile() && data.action !== 'reject' ? 2000 : 0 );
return delay.promise();
} else {
ve.track( 'counter.editcheck.preSaveChecksAbandoned' );
return ve.createDeferred().reject().promise();
}
} );
if ( data ) {
const delay = ve.createDeferred();
// If they inserted, wait 2 seconds on desktop
// before showing save dialog to make sure insertions are finialized
setTimeout( () => {
ve.track( 'counter.editcheck.preSaveChecksCompleted' );
delay.resolve();
}, !OO.ui.isMobile() && data.action !== 'reject' ? 2000 : 0 );
return delay.promise();
} else {
// closed via "back" or otherwise
ve.track( 'counter.editcheck.preSaveChecksAbandoned' );
return ve.createDeferred().reject().promise();
}
} );
} );
} else {
// Counterpart to earlier preSaveChecksShown, for use in tracking
@ -265,11 +197,6 @@ if ( mw.config.get( 'wgVisualEditorConfig' ).editCheck || mw.editcheck.ecenable
ve.track( 'counter.editcheck.preSaveChecksNotShown' );
}
} );
mw.hook( 've.deactivationComplete' ).add( () => {
if ( saveProcessDeferred ) {
saveProcessDeferred.reject();
}
} );
}
ve.ui.EditCheckBack = function VeUiEditCheckBack() {
@ -286,12 +213,9 @@ ve.ui.EditCheckBack.static.autoAddToGroup = false;
ve.ui.EditCheckBack.static.title =
OO.ui.deferMsg( 'visualeditor-backbutton-tooltip' );
ve.ui.EditCheckBack.prototype.onSelect = function () {
const context = this.toolbar.getSurface().getContext();
if ( context.inspector ) {
context.inspector.close();
} else {
context.items[ 0 ].close();
}
const surface = this.toolbar.getSurface();
surface.getContext().hide();
surface.execute( 'window', 'close', 'editCheckDialog' );
this.setActive( false );
};
ve.ui.EditCheckBack.prototype.onUpdateState = function () {

View file

@ -136,6 +136,10 @@
"value": false,
"description": "Enable experimental Edit Check feature. Can also be enabled using ?ecenable=1."
},
"VisualEditorEditCheckSingleCheckMode": {
"value": true,
"description": "Only allow a single edit check to be surfaced"
},
"VisualEditorEditCheckABTest": {
"value": false,
"description": "A/B test Edit Check for all users. A/B bucket status will override VisualEditorEditCheck."
@ -647,7 +651,10 @@
"editcheck-dialog-addref-success-notify",
"editcheck-dialog-addref-title",
"editcheck-dialog-title",
"visualeditor-backbutton-tooltip"
"editcheck-review-title",
"visualeditor-backbutton-tooltip",
"next",
"last"
]
},
"ext.visualEditor.core.utils": {

View file

@ -1159,6 +1159,7 @@ class Hooks implements
'useChangeTagging' => $veConfig->get( 'VisualEditorUseChangeTagging' ),
'editCheckTagging' => $veConfig->get( 'VisualEditorEditCheckTagging' ),
'editCheck' => $veConfig->get( 'VisualEditorEditCheck' ),
'editCheckSingle' => $veConfig->get( 'VisualEditorEditCheckSingleCheckMode' ),
'editCheckABTest' => $veConfig->get( 'VisualEditorEditCheckABTest' ),
'editCheckReliabilityAvailable' => ApiEditCheckReferenceUrl::isAvailable(),
'namespacesWithSubpages' => $namespacesWithSubpagesEnabled,