/*! * 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() { // Pre-initialization this.$element.addClass( 've-ui-editCheckDialog' ); }; /* Inheritance */ OO.initClass( ve.ui.EditCheckDialog ); /* Static Properties */ // TODO: Keep surface active on mobile for some checks? ve.ui.EditCheckDialog.static.activeSurface = !OO.ui.isMobile(); // Invisible title for accessibility ve.ui.EditCheckDialog.static.title = OO.ui.deferMsg( 'editcheck-review-title' ); /* Methods */ /** * @inheritdoc */ ve.ui.EditCheckDialog.prototype.initialize = function () { 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 = null; this.currentActions = null; 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.$actions = $( '
' ); if ( OO.ui.isMobile() ) { this.$body.append( this.title.$element ); } this.$body.append( this.closeButton.$element, this.$actions, this.footer.$element ); }; ve.ui.EditCheckDialog.prototype.onActionsUpdated = function ( listener, actions, newActions, discardedActions, rejected ) { if ( listener !== this.listener ) { return; } if ( this.updateFilter ) { actions = this.updateFilter( actions, newActions, discardedActions, this.currentActions ); } this.showActions( actions, newActions, rejected ); }; ve.ui.EditCheckDialog.prototype.showActions = function ( actions, newActions, lastActionRejected ) { this.currentActions = actions; if ( actions.length === 0 ) { return this.close( { action: lastActionRejected ? 'reject' : 'complete' } ); } // This just adjusts so the previously selected check remains selected: let newOffset = Math.min( this.currentOffset, actions.length - 1 ); if ( newActions.length ) { newOffset = actions.indexOf( newActions[ 0 ] ); } this.refresh(); this.setCurrentOffset( newOffset, false ); }; ve.ui.EditCheckDialog.prototype.hasAction = function ( action ) { return this.currentActions.some( ( caction ) => action.equals( caction ) ); }; ve.ui.EditCheckDialog.prototype.refresh = function () { this.$actions.empty(); this.currentActions.forEach( ( action, index ) => { const widget = action.render( index !== this.currentOffset, this.singleAction, this.surface ); widget.on( 'togglecollapse', this.onToggleCollapse, [ action, index ], this ); action.off( 'act' ).on( 'act', this.onAct, [ action, widget ], this ); this.$actions.append( widget.$element ); // for scrolling later action.widget = widget; } ); }; /** * Set the offset of the current check, within the list of all checks * * @param {number|null} offset * @param {boolean} fromUserAction * @param {boolean} internal */ ve.ui.EditCheckDialog.prototype.setCurrentOffset = function ( offset, fromUserAction, internal ) { // TODO: work out how to tell the window to recalculate height here if ( offset === null || offset === -1 ) { /* That's valid, carry on */ offset = null; } else if ( !Number.isSafeInteger( offset ) || ( offset < 0 || offset > ( this.currentActions.length - 1 ) ) ) { throw new Error( `Bad offset ${ offset }, expected an integer between 0 and ${ this.currentActions.length - 1 }` ); } this.currentOffset = offset; this.$body.find( '.ve-ui-editCheckActionWidget' ).each( ( i, el ) => { $( el ).toggleClass( 've-ui-editCheckActionWidget-collapsed', i !== this.currentOffset ); } ); if ( this.currentOffset !== null ) { this.footerLabel.setLabel( ve.msg( 'visualeditor-find-and-replace-results', ve.init.platform.formatNumber( this.currentOffset + 1 ), ve.init.platform.formatNumber( this.currentActions.length ) ) ); } else { this.footerLabel.setLabel( '' ); } this.nextButton.setDisabled( this.currentOffset !== null && this.currentOffset >= this.currentActions.length - 1 ); this.previousButton.setDisabled( this.currentOffset === null || this.currentOffset <= 0 ); this.updateSize(); if ( !internal ) { this.controller.focusAction( this.currentActions[ this.currentOffset ], fromUserAction ); } }; ve.ui.EditCheckDialog.prototype.onFocusAction = function ( action, index, scrollTo ) { this.setCurrentOffset( this.currentActions.indexOf( action ), scrollTo, true ); }; /** * @inheritdoc */ ve.ui.EditCheckDialog.prototype.getSetupProcess = function ( data, process ) { return process.first( () => { this.controller = data.controller; this.controller.on( 'actionsUpdated', this.onActionsUpdated, false, this ); this.controller.on( 'focusAction', this.onFocusAction, false, this ); const actions = data.actions || this.controller.getActions( this.listener ); this.listener = data.listener || 'onDocumentChange'; this.surface = data.surface; this.updateFilter = data.updateFilter; this.singleAction = ( this.listener === 'onBeforeSave' ) || OO.ui.isMobile(); this.closeButton.toggle( OO.ui.isMobile() ); if ( data.footer !== undefined ) { this.footer.toggle( data.footer ); } else { this.footer.toggle( this.singleAction && // If we're in single-check mode don't show even the disabled pagers: !mw.config.get( 'wgVisualEditorConfig' ).editCheckSingle ); } this.$element.toggleClass( 've-ui-editCheckDialog-singleAction', this.singleAction ); this.surface.context.hide(); this.showActions( actions, actions ); }, this ); }; /** * @inheritdoc */ ve.ui.EditCheckDialog.prototype.getTeardownProcess = function ( data, process ) { return process.next( () => { this.controller.off( 'actionsUpdated', this.onActionsUpdated, this ); this.controller.off( 'focusAction', this.onFocusAction, this ); this.$actions.empty(); }, this ); }; /** * Handle 'act' events from the mw.widget.EditCheckActionWidget * * @param {mw.editcheck.EditCheckAction} action * @param {mw.editcheck.EditCheckActionWidget} widget * @param {jQuery.Promise} promise Promise which resolves when the action is complete */ ve.ui.EditCheckDialog.prototype.onAct = function ( action, widget, 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 ); if ( data && this.listener === 'onBeforeSave' ) { // If an action has been taken, we want to linger for a brief moment // to show the result of the action before moving away // TODO: This was written for AddReferenceEditCheck but should be // more generic const pause = data.action !== 'reject' ? 500 : 0; setTimeout( () => { const rejected = [ 'feedback', 'reject', 'dismiss' ].includes( data.action ); this.controller.removeAction( this.listener, action, rejected ); }, pause ); } else { this.controller.refresh(); } } ); }; /** * Handle 'togglecollapse' events from the mw.widget.EditCheckActionWidget * * @param {mw.editcheck.EditCheckAction} action * @param {number} index * @param {boolean} collapsed */ ve.ui.EditCheckDialog.prototype.onToggleCollapse = function ( action, index, collapsed ) { if ( !collapsed ) { // expanded one this.setCurrentOffset( this.currentActions.indexOf( action ), true ); if ( !OO.ui.isMobile() ) { const surfaceModel = this.surface.getModel(); const checkRange = action.getFocusSelection().getCoveringRange(); const surfaceRange = surfaceModel.getSelection().getCoveringRange(); // Collapse and move the selection to the nearest part of the check range // Don't alter it if it touches the check range if ( surfaceRange === null || surfaceRange.end < checkRange.start ) { surfaceModel.setLinearSelection( new ve.Range( checkRange.start ) ); this.surface.getView().activate(); this.surface.getView().focus(); } else if ( surfaceRange.start > checkRange.end ) { surfaceModel.setLinearSelection( new ve.Range( checkRange.end ) ); this.surface.getView().activate(); this.surface.getView().focus(); } } } }; /** * 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 === null ? 0 : this.currentOffset + 1, true ); }; /** * Handle click events from the previous button */ ve.ui.EditCheckDialog.prototype.onPreviousButtonClick = function () { this.setCurrentOffset( this.currentOffset === null ? this.currentActions.length - 1 : this.currentOffset - 1, true ); }; ve.ui.SidebarEditCheckDialog = function VeUiSidebarEditCheckDialog( config ) { // Parent constructor ve.ui.SidebarEditCheckDialog.super.call( this, config ); // Mixin constructor ve.ui.EditCheckDialog.call( this, config ); }; OO.inheritClass( ve.ui.SidebarEditCheckDialog, ve.ui.SidebarDialog ); OO.mixinClass( ve.ui.SidebarEditCheckDialog, ve.ui.EditCheckDialog ); ve.ui.SidebarEditCheckDialog.static.name = 'sidebarEditCheckDialog'; ve.ui.SidebarEditCheckDialog.static.size = 'medium'; ve.ui.SidebarEditCheckDialog.prototype.initialize = function () { // Parent method ve.ui.SidebarEditCheckDialog.super.prototype.initialize.call( this ); // Mixin method return ve.ui.EditCheckDialog.prototype.initialize.call( this ); }; ve.ui.SidebarEditCheckDialog.prototype.getSetupProcess = function ( data ) { // Parent method const process = ve.ui.SidebarEditCheckDialog.super.prototype.getSetupProcess.call( this, data ); // Mixin method return ve.ui.EditCheckDialog.prototype.getSetupProcess.call( this, data, process ).next( () => { this.controller.on( 'position', this.onPosition, null, this ); } ); }; ve.ui.SidebarEditCheckDialog.prototype.getTeardownProcess = function ( data ) { // Parent method const process = ve.ui.SidebarEditCheckDialog.super.prototype.getTeardownProcess.call( this, data ); // Mixin method return ve.ui.EditCheckDialog.prototype.getTeardownProcess.call( this, data, process ).next( () => { this.controller.off( 'position', this.onPosition, this ); } ); }; ve.ui.SidebarEditCheckDialog.prototype.onPosition = function () { if ( this.listener === 'onBeforeSave' ) { return; } const surfaceView = this.surface.getView(); const surfaceTop = surfaceView.$element.offset().top + 10; this.currentActions.forEach( ( action ) => { const widget = action.widget; if ( widget ) { let top = Infinity; action.getHighlightSelections().forEach( ( selection ) => { const selectionView = ve.ce.Selection.static.newFromModel( selection, surfaceView ); const rect = selectionView.getSelectionBoundingRect(); if ( !rect ) { return; } top = Math.min( top, rect.top ); } ); widget.$element.css( 'margin-top', '' ); widget.$element.css( 'margin-top', Math.max( 0, top + surfaceTop - widget.$element.offset().top ) ); } } ); }; ve.ui.SidebarEditCheckDialog.prototype.onToggleCollapse = function () { // mixin ve.ui.EditCheckDialog.prototype.onToggleCollapse.apply( this, arguments ); this.onPosition(); }; /* Registration */ ve.ui.windowFactory.register( ve.ui.SidebarEditCheckDialog ); ve.ui.FixedEditCheckDialog = function VeUiFixedEditCheckDialog( config ) { // Parent constructor ve.ui.FixedEditCheckDialog.super.call( this, config ); // Mixin constructor ve.ui.EditCheckDialog.call( this, config ); }; OO.inheritClass( ve.ui.FixedEditCheckDialog, ve.ui.ToolbarDialog ); OO.mixinClass( ve.ui.FixedEditCheckDialog, ve.ui.EditCheckDialog ); ve.ui.FixedEditCheckDialog.static.name = 'fixedEditCheckDialog'; ve.ui.FixedEditCheckDialog.static.position = OO.ui.isMobile() ? 'below' : 'side'; ve.ui.FixedEditCheckDialog.static.size = OO.ui.isMobile() ? 'full' : 'medium'; ve.ui.FixedEditCheckDialog.prototype.initialize = function () { // Parent method ve.ui.FixedEditCheckDialog.super.prototype.initialize.call( this ); // Mixin method return ve.ui.EditCheckDialog.prototype.initialize.call( this ); }; ve.ui.FixedEditCheckDialog.prototype.getSetupProcess = function ( data ) { // Parent method const process = ve.ui.FixedEditCheckDialog.super.prototype.getSetupProcess.call( this, data ); // Mixin method return ve.ui.EditCheckDialog.prototype.getSetupProcess.call( this, data, process ); }; ve.ui.FixedEditCheckDialog.prototype.getTeardownProcess = function ( data ) { // Parent method const process = ve.ui.FixedEditCheckDialog.super.prototype.getTeardownProcess.call( this, data ); // Mixin method return ve.ui.EditCheckDialog.prototype.getTeardownProcess.call( this, data, process ); }; ve.ui.FixedEditCheckDialog.prototype.onFocusAction = function ( action, index, scrollTo ) { if ( this.singleAction && action === null ) { // Can't unset the offset in single-action mode, because it hides the sidebar contents return; } // Mixin method return ve.ui.EditCheckDialog.prototype.onFocusAction.call( this, action, index, scrollTo ); }; /* Registration */ ve.ui.windowFactory.register( ve.ui.FixedEditCheckDialog ); ve.ui.commandRegistry.register( new ve.ui.Command( 'editCheckDialogInProcessOpen', 'window', 'open', { args: [ 'editCheckDialog', { listener: 'onDocumentChange' } ] } ) ); ve.ui.commandRegistry.register( new ve.ui.Command( 'editCheckDialogInProcessToggle', 'window', 'toggle', { args: [ 'editCheckDialog', { listener: 'onDocumentChange' } ] } ) ); ve.ui.commandRegistry.register( new ve.ui.Command( 'editCheckDialogBeforeSave', 'window', 'toggle', { args: [ 'editCheckDialog', { listener: 'onBeforeSave' } ] } ) ); /** * @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 = 'editCheckDialogInProcessToggle'; // ve.ui.EditCheckDialogTool.static.commandName = 'editCheckDialogBeforeSave'; // Demo button for opening edit check sidebar // ve.ui.toolFactory.register( ve.ui.EditCheckDialogTool );