mediawiki-extensions-Visual.../modules/ve-mw/ui/dialogs/ve.ui.MWTransclusionDialog.js
WMDE-Fisch cede096333 Update accept button in confirmation dialog
The only places where this dialog is used now will have the same
button labels and format. We want to use "normal" destructive here
so no additional "primary" styling.

It seems that the focus on the action will only be applied
automatically when it is primary. So extra code is needed.

Bug: T299647
Change-Id: Ib5250b79e85d27ea197b83c6380863d0749e5d89
2022-01-21 15:32:58 +00:00

660 lines
21 KiB
JavaScript

/*!
* VisualEditor user interface MWTransclusionDialog class.
*
* @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Dialog for inserting and editing MediaWiki transclusions, i.e. a sequence of one or more template
* invocations that strictly belong to each other (e.g. because they are unbalanced), possibly
* mixed with raw wikitext snippets.
*
* Note the base class {@see ve.ui.MWTemplateDialog} alone does not allow to manage more than a
* single template invocation. Most of the code for this feature set is exclusive to this subclass.
*
* @class
* @extends ve.ui.MWTemplateDialog
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.ui.MWTransclusionDialog = function VeUiMWTransclusionDialog( config ) {
var veConfig = mw.config.get( 'wgVisualEditorConfig' );
// Parent constructor
ve.ui.MWTransclusionDialog.super.call( this, config );
// Properties
this.isSidebarExpanded = null;
this.setupHotkeyTriggers();
// Temporary feature flags
this.useInlineDescriptions = veConfig.transclusionDialogInlineDescriptions;
this.useBackButton = veConfig.transclusionDialogBackButton;
this.useSearchImprovements = veConfig.templateSearchImprovements;
this.useNewSidebar = veConfig.transclusionDialogNewSidebar;
if ( this.useInlineDescriptions ) {
this.$element.addClass( 've-ui-mwTransclusionDialog-bigger' );
}
if ( this.useSearchImprovements ) {
this.$element.addClass( 've-ui-mwTransclusionDialog-enhancedSearch' );
}
if ( this.useNewSidebar ) {
this.$element.addClass( 've-ui-mwTransclusionDialog-newSidebar' );
}
this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWTransclusionDialog, ve.ui.MWTemplateDialog );
/* Static Properties */
ve.ui.MWTransclusionDialog.static.name = 'transclusion';
ve.ui.MWTransclusionDialog.static.actions = ve.ui.MWTemplateDialog.static.actions.concat( [
{
action: 'mode',
modes: [ 'edit', 'insert' ],
// HACK: Will be set later, but we want measurements to be accurate in the mean time, this
// will not be needed when T93290 is resolved
label: $( document.createTextNode( '\u00a0' ) )
},
{
action: 'back',
label: OO.ui.deferMsg( 'visualeditor-dialog-action-goback' ),
modes: [ 'edit', 'insert' ],
flags: [ 'safe', 'back' ]
}
] );
/** @inheritdoc */
ve.ui.MWTransclusionDialog.static.bookletLayoutConfig = ve.extendObject(
{},
ve.ui.MWTemplateDialog.static.bookletLayoutConfig,
{ outlined: true, editable: true }
);
ve.ui.MWTransclusionDialog.static.smallScreenMaxWidth = 540;
/* Methods */
/**
* Handle outline controls move events.
*
* @private
* @param {number} places Number of places to move the selected item
*/
ve.ui.MWTransclusionDialog.prototype.onOutlineControlsMove = function ( places ) {
var part = this.transclusionModel.getPartFromId( this.findSelectedItemId() );
if ( !part ) {
return;
}
var index = this.transclusionModel.getParts().indexOf( part );
// Move part to new location, and if dialog is loaded switch to new part page
var promise = this.transclusionModel.addPart( part, index + places );
if ( this.loaded && !this.preventReselection ) {
promise.done( this.focusPart.bind( this, part.getId() ) );
}
};
/**
* Handle outline controls remove events.
*
* @private
*/
ve.ui.MWTransclusionDialog.prototype.onOutlineControlsRemove = function () {
var itemId = this.findSelectedItemId(),
part = this.transclusionModel.getPartFromId( itemId );
// Check if the part is the actual template, or one of its parameters
// TODO: This applies to the old sidebar only and can be removed later
if ( part instanceof ve.dm.MWTemplateModel && itemId !== part.getId() ) {
var param = part.getParameterFromId( itemId );
if ( param ) {
param.remove();
}
} else if ( part instanceof ve.dm.MWTransclusionPartModel ) {
this.transclusionModel.removePart( part );
}
};
/**
* Create a new template part at the end of the transclusion.
*
* @private
*/
ve.ui.MWTransclusionDialog.prototype.addTemplatePlaceholder = function () {
this.addPart( new ve.dm.MWTemplatePlaceholderModel( this.transclusionModel ) );
};
/**
* Handle add content button click events.
*
* @private
*/
ve.ui.MWTransclusionDialog.prototype.addContent = function () {
this.addPart( new ve.dm.MWTransclusionContentModel( this.transclusionModel ) );
};
/**
* Handle add parameter button click events.
*
* @private
*/
ve.ui.MWTransclusionDialog.prototype.addParameter = function () {
var part = this.transclusionModel.getPartFromId( this.findSelectedItemId() );
if ( !( part instanceof ve.dm.MWTemplateModel ) ) {
return;
}
// TODO: Use a distinct class for placeholder model rather than
// these magical "empty" constants.
var placeholderParameter = new ve.dm.MWParameterModel( part );
part.addParameter( placeholderParameter );
this.focusPart( placeholderParameter.getId() );
if ( this.useInlineDescriptions ) {
this.autoExpandSidebar();
}
};
/**
* Handle booklet layout page set events.
*
* @private
* @param {OO.ui.PageLayout} page Active page
*/
ve.ui.MWTransclusionDialog.prototype.onBookletLayoutSetPage = function ( page ) {
var isLastPlaceholder = page instanceof ve.ui.MWTemplatePlaceholderPage &&
this.transclusionModel.isSingleTemplate(),
acceptsNewParameters = page instanceof ve.ui.MWTemplatePage ||
page instanceof ve.ui.MWParameterPage;
this.addParameterButton.setDisabled( !acceptsNewParameters || this.isReadOnly() );
this.bookletLayout.getOutlineControls().removeButton.toggle( !isLastPlaceholder );
if ( this.pocSidebar ) {
this.pocSidebar.setSelectionByPageName( page.getName() );
}
};
/**
* @inheritdoc
*/
ve.ui.MWTransclusionDialog.prototype.onReplacePart = function ( removed, added ) {
ve.ui.MWTransclusionDialog.super.prototype.onReplacePart.call( this, removed, added );
var parts = this.transclusionModel.getParts();
if ( parts.length === 0 ) {
this.addParameterButton.setDisabled( true );
this.addPart( new ve.dm.MWTemplatePlaceholderModel( this.transclusionModel ) );
} else if ( parts.length > 1 ) {
this.bookletLayout.getOutlineControls().toggle( true );
this.$element.removeClass( 've-ui-mwTransclusionDialog-single-transclusion' );
}
// multipart message
if ( this.useNewSidebar ) {
this.bookletLayout.stackLayout.$element.prepend( this.multipartMessage.$element );
this.multipartMessage.toggle( parts.length > 1 );
}
this.autoExpandSidebar();
this.updateModeActionState();
this.updateActionSet();
};
/**
* @private
*/
ve.ui.MWTransclusionDialog.prototype.setupHotkeyTriggers = function () {
var meta = ve.getSystemPlatform() === 'mac' ? 'meta+' : 'ctrl+';
this.hotkeyTriggers = {};
this.hotkeyTriggers[ meta + 'd' ] = this.addTemplatePlaceholder.bind( this );
this.hotkeyTriggers[ meta + 'shift+y' ] = this.addContent.bind( this );
this.hotkeyTriggers[ meta + 'shift+d' ] = this.addParameter.bind( this );
};
/**
* Handles key down events.
*
* @protected
* @param {jQuery.Event} e Key down event
*/
ve.ui.MWTransclusionDialog.prototype.onKeyDown = function ( e ) {
var eventTrigger = new ve.ui.Trigger( e ).toString();
if ( this.hotkeyTriggers[ eventTrigger ] ) {
this.hotkeyTriggers[ eventTrigger ]();
e.preventDefault();
e.stopPropagation();
}
};
/**
* @return {string|undefined} Any id, including slash-delimited template parameter ids
*/
ve.ui.MWTransclusionDialog.prototype.findSelectedItemId = function () {
if ( this.pocSidebar ) {
// TODO: This can't return parameter ids any more when the old sidebar is gone
return this.pocSidebar.findSelectedPartId();
}
var item = this.bookletLayout.getOutline().findSelectedItem();
return item && item.getData();
};
/**
* @inheritdoc
*/
ve.ui.MWTransclusionDialog.prototype.getPageFromPart = function ( part ) {
var page = ve.ui.MWTransclusionDialog.super.prototype.getPageFromPart.call( this, part );
if ( !page && part instanceof ve.dm.MWTransclusionContentModel ) {
return new ve.ui.MWTransclusionContentPage( part, part.getId(), { $overlay: this.$overlay, isReadOnly: this.isReadOnly() } );
}
return page;
};
/**
* Automatically expand or collapse the sidebar according to default logic.
*
* @protected
*/
ve.ui.MWTransclusionDialog.prototype.autoExpandSidebar = function () {
var expandSidebar;
if ( this.useInlineDescriptions ) {
var isSmallScreen = this.isNarrowScreen();
var showOtherActions = isSmallScreen ||
this.actions.getOthers().some( function ( action ) {
// Check for unknown actions, show the toolbar if any are available.
return action.action !== 'mode';
} );
this.actions.forEach( { actions: [ 'mode' ] }, function ( action ) {
action.toggle( isSmallScreen );
} );
this.$otherActions.toggleClass( 'oo-ui-element-hidden', !showOtherActions );
if ( isSmallScreen && this.transclusionModel.isEmpty() ) {
expandSidebar = false;
} else if ( isSmallScreen &&
// eslint-disable-next-line no-jquery/no-class-state
this.$content.hasClass( 've-ui-mwTransclusionDialog-small-screen' )
) {
// We did this already. If the sidebar is visible or not is now the user's decision.
return;
} else {
expandSidebar = !isSmallScreen;
}
this.$content.toggleClass( 've-ui-mwTransclusionDialog-small-screen', isSmallScreen );
} else {
expandSidebar = !this.transclusionModel.isSingleTemplate();
}
this.toggleSidebar( expandSidebar );
};
/**
* Set if the sidebar is visible (which means the dialog is expanded), or collapsed.
*
* @param {boolean} expandSidebar
* @private
*/
ve.ui.MWTransclusionDialog.prototype.toggleSidebar = function ( expandSidebar ) {
if ( this.isSidebarExpanded === expandSidebar ) {
return;
}
this.isSidebarExpanded = expandSidebar;
this.$content
.toggleClass( 've-ui-mwTransclusionDialog-collapsed', !expandSidebar )
.toggleClass( 've-ui-mwTransclusionDialog-expanded', expandSidebar );
var dialogSizeSidebarExpanded = this.useInlineDescriptions ? 'larger' : 'large';
var dialogSizeSidebarCollapsed = this.useNewSidebar ? dialogSizeSidebarExpanded : 'medium';
this.ignoreNextWindowResizeEvent = true;
this.setSize( expandSidebar ? dialogSizeSidebarExpanded : dialogSizeSidebarCollapsed );
this.bookletLayout.toggleOutline( expandSidebar );
this.updateTitle();
this.updateModeActionState();
// HACK blur any active input so that its dropdown will be hidden and won't end
// up being mispositioned
this.$content.find( 'input:focus' ).trigger( 'blur' );
if ( this.useInlineDescriptions && this.pocSidebar && this.loaded && this.isNarrowScreen() ) {
// Reapply selection and scrolling when switching between panes.
// FIXME: decouple from descendants
var selectedPage = this.bookletLayout.stackLayout.getCurrentItem();
if ( selectedPage ) {
var name = selectedPage.getName();
var dialog = this;
// Align whichever panel is becoming visible, after animation completes.
// TODO: Should hook onto an animation promise—but is this possible when pure CSS?
setTimeout( function () {
if ( expandSidebar ) {
dialog.pocSidebar.setSelectionByPageName( name );
} else {
selectedPage.scrollElementIntoView();
// TODO: Find a reliable way to refocus.
// dialog.focusPart( name );
}
}, OO.ui.theme.getDialogTransitionDuration() );
}
}
};
ve.ui.MWTransclusionDialog.prototype.isNarrowScreen = function () {
return $( window ).width() <= this.constructor.static.smallScreenMaxWidth;
};
/**
* @inheritdoc
*/
ve.ui.MWTransclusionDialog.prototype.updateTitle = function () {
if ( !this.transclusionModel.isSingleTemplate() ) {
this.title.setLabel( ve.msg( 'visualeditor-dialog-transclusion-title-edit-transclusion' ) );
} else {
// Parent method
ve.ui.MWTransclusionDialog.super.prototype.updateTitle.call( this );
}
};
/**
* Update the state of the 'mode' action
*
* @private
*/
ve.ui.MWTransclusionDialog.prototype.updateModeActionState = function () {
var isExpanded = this.isSidebarExpanded,
label = ve.msg( isExpanded ?
'visualeditor-dialog-transclusion-collapse-options' :
'visualeditor-dialog-transclusion-expand-options' );
this.actions.forEach( { actions: [ 'mode' ] }, function ( action ) {
action.setLabel( label );
action.$button.attr( 'aria-expanded', isExpanded ? 1 : 0 );
} );
// Old sidebar: Only a single template can be collapsed, except it's still the initial
// placeholder.
// New sidebar: The button is only visible on very narrow screens, {@see autoExpandSidebar}.
// It's always needed, except in the initial placeholder state.
var isInitialState = !isExpanded && this.transclusionModel.isEmpty(),
canCollapse = ( this.transclusionModel.isSingleTemplate() || this.useInlineDescriptions ) &&
!isInitialState;
this.actions.setAbilities( { mode: canCollapse } );
};
/**
* Add a part to the transclusion.
*
* @param {ve.dm.MWTransclusionPartModel} part Part to add
*/
ve.ui.MWTransclusionDialog.prototype.addPart = function ( part ) {
var parts = this.transclusionModel.getParts(),
selectedPart = this.transclusionModel.getPartFromId( this.findSelectedItemId() );
// Insert after selected part, or at the end if nothing is selected
var index = selectedPart ? parts.indexOf( selectedPart ) + 1 : parts.length;
// Add the part, and if dialog is loaded switch to part page
var promise = this.transclusionModel.addPart( part, index );
if ( this.loaded && !this.preventReselection ) {
promise.done( this.focusPart.bind( this, part.getId() ) );
}
};
/**
* @inheritdoc
*/
ve.ui.MWTransclusionDialog.prototype.getActionProcess = function ( action ) {
if ( action === 'back' ) {
return new OO.ui.Process( function () {
if ( this.transclusionModel.containsValuableData() ) {
ve.ui.MWConfirmationDialog.confirm(
'visualeditor-dialog-transclusion-back-confirmation-prompt',
this.resetDialog.bind( this )
);
} else {
this.resetDialog();
}
}, this );
}
if ( action === 'mode' ) {
return new OO.ui.Process( function () {
this.toggleSidebar( !this.isSidebarExpanded );
}, this );
}
if ( !action ) {
if ( this.altered && this.transclusionModel.containsValuableData() ) {
return new OO.ui.Process( function () {
ve.ui.MWConfirmationDialog.confirm(
'visualeditor-dialog-transclusion-close-confirmation-prompt',
this.close.bind( this ) );
}, this );
}
}
return ve.ui.MWTransclusionDialog.super.prototype.getActionProcess.call( this, action );
};
/**
* Update the widgets in the dialog's action bar.
*
* @private
*/
ve.ui.MWTransclusionDialog.prototype.updateActionSet = function () {
var backButton = this.actions.get( { flags: [ 'back' ] } ).pop(),
saveButton = this.actions.get( { actions: [ 'done' ] } ).pop();
if ( saveButton && this.getMode() === 'edit' ) {
saveButton.setLabel( ve.msg( 'visualeditor-dialog-transclusion-action-save' ) );
}
if ( this.useBackButton ) {
var closeButton = this.actions.get( { flags: [ 'close' ] } ).pop(),
canGoBack = this.getMode() === 'insert' && !this.transclusionModel.isEmpty();
closeButton.toggle( !canGoBack );
backButton.toggle( canGoBack );
} else {
backButton.toggle( false );
}
};
/**
* Revert the dialog back to its initial state.
*
* @private
*/
ve.ui.MWTransclusionDialog.prototype.resetDialog = function () {
var target = this;
this.transclusionModel.reset();
if ( this.pocSidebar ) {
this.pocSidebar.clear();
}
this.bookletLayout.clearPages();
this.transclusionModel
.addPart( new ve.dm.MWTemplatePlaceholderModel( this.transclusionModel ), 0 )
.done( function () {
target.autoExpandSidebar();
} );
};
/**
* @inheritdoc
*/
ve.ui.MWTransclusionDialog.prototype.initialize = function () {
// Parent method
ve.ui.MWTransclusionDialog.super.prototype.initialize.call( this );
// Properties
this.addTemplateButton = new OO.ui.ButtonWidget( {
framed: false,
icon: 'puzzle',
title: ve.msg( 'visualeditor-dialog-transclusion-add-template' )
} );
this.addContentButton = new OO.ui.ButtonWidget( {
framed: false,
icon: 'wikiText',
title: ve.msg( this.useNewSidebar ?
'visualeditor-dialog-transclusion-add-wikitext' :
'visualeditor-dialog-transclusion-add-content' )
} );
this.addParameterButton = new OO.ui.ButtonWidget( {
framed: false,
icon: 'parameter',
title: ve.msg( 'visualeditor-dialog-transclusion-add-param' )
} );
this.bookletLayout.getOutlineControls().addItems( [ this.addTemplateButton, this.addContentButton ] );
if ( !this.useNewSidebar ) {
this.bookletLayout.getOutlineControls().addItems( [ this.addParameterButton ] );
}
// multipart message gets attached in onReplacePart()
this.multipartMessage = new OO.ui.MessageWidget( {
label: mw.message( 'visualeditor-dialog-transclusion-multipart-message' ).parseDom(),
classes: [ 've-ui-mwTransclusionDialog-multipart-message' ]
} );
ve.targetLinksToNewWindow( this.multipartMessage.$element[ 0 ] );
if ( this.useNewSidebar || this.useInlineDescriptions ) {
var helpPopup = new ve.ui.MWFloatingHelpElement( {
label: mw.message( 'visualeditor-dialog-transclusion-help-title' ).text(),
title: mw.message( 'visualeditor-dialog-transclusion-help-title' ).text(),
$message: new OO.ui.FieldsetLayout( {
items: [
new OO.ui.LabelWidget( {
label: mw.message( 'visualeditor-dialog-transclusion-help-message' ).text()
} ),
this.getMessageButton( 'visualeditor-dialog-transclusion-help-page-help', 'helpNotice' ),
this.getMessageButton( 'visualeditor-dialog-transclusion-help-page-feedback', 'feedback' )
],
classes: [ 've-ui-mwTransclusionDialog-floatingHelpElement-fieldsetLayout' ]
} ).$element
} );
helpPopup.$element.addClass( 've-ui-mwTransclusionDialog-floatingHelpElement' );
helpPopup.$element.appendTo( this.$body );
}
// Events
if ( this.useInlineDescriptions ) {
this.getManager().connect( this, { resize: ve.debounce( this.onWindowResize.bind( this ) ) } );
}
this.bookletLayout.connect( this, { set: 'onBookletLayoutSetPage' } );
this.bookletLayout.$menu.find( '[ role="listbox" ]' ).first()
.attr( 'aria-label', ve.msg( 'visualeditor-dialog-transclusion-templates-menu-aria-label' ) );
this.addTemplateButton.connect( this, { click: 'addTemplatePlaceholder' } );
this.addContentButton.connect( this, { click: 'addContent' } );
this.addParameterButton.connect( this, { click: 'addParameter' } );
this.bookletLayout.getOutlineControls().connect( this, {
move: 'onOutlineControlsMove',
remove: 'onOutlineControlsRemove'
} );
};
/**
* @inheritdoc
*/
ve.ui.MWTransclusionDialog.prototype.getSetupProcess = function ( data ) {
this.onTearDownCallback = data && data.onTearDownCallback;
return ve.ui.MWTransclusionDialog.super.prototype.getSetupProcess.call( this, data )
.next( function () {
var isReadOnly = this.isReadOnly();
this.addTemplateButton.setDisabled( isReadOnly );
this.addContentButton.setDisabled( isReadOnly );
this.addParameterButton.setDisabled( isReadOnly );
this.bookletLayout.getOutlineControls().setAbilities( {
move: !isReadOnly,
remove: !isReadOnly
} );
if ( this.useNewSidebar ) {
this.bookletLayout.getOutlineControls().toggle( !this.transclusionModel.isSingleTemplate() );
this.$element.toggleClass(
've-ui-mwTransclusionDialog-single-transclusion',
this.transclusionModel.isSingleTemplate()
);
}
this.updateModeActionState();
this.autoExpandSidebar();
}, this );
};
/** @inheritdoc */
ve.ui.MWTransclusionDialog.prototype.getTeardownProcess = function () {
if ( this.onTearDownCallback ) {
this.onTearDownCallback();
}
return ve.ui.MWTransclusionDialog.super.prototype.getTeardownProcess.apply( this, arguments );
};
/**
* @private
*/
ve.ui.MWTransclusionDialog.prototype.onWindowResize = function () {
if ( this.transclusionModel && !this.ignoreNextWindowResizeEvent ) {
this.autoExpandSidebar();
}
this.ignoreNextWindowResizeEvent = false;
};
/**
* @inheritdoc
*
* Temporary override to increase dialog size when a feature flag is enabled.
*/
ve.ui.MWTransclusionDialog.prototype.getSizeProperties = function () {
var sizeProps = ve.ui.MWTransclusionDialog.super.prototype.getSizeProperties.call( this );
if ( this.useInlineDescriptions ) {
sizeProps = ve.extendObject( { height: '90%' }, sizeProps );
}
return sizeProps;
};
/**
* Converts a message link into an OO.ui.ButtonWidget with an icon.
*
* @private
* @param {string} message i18n message key
* @param {string} icon icon name
* @return {OO.ui.ButtonWidget}
*/
ve.ui.MWTemplateDialog.prototype.getMessageButton = function ( message, icon ) {
// Messages that can be used here:
// * visualeditor-dialog-transclusion-help-page-help
// * visualeditor-dialog-transclusion-help-page-feedback
var $link = mw.message( message ).parseDom(),
button = new OO.ui.ButtonWidget( {
label: $link.text(),
href: $link.attr( 'href' ),
target: '_blank',
flags: 'progressive',
icon: icon,
framed: false
} );
button.$button.attr( 'role', 'link' );
return button;
};
/* Registration */
ve.ui.windowFactory.register( ve.ui.MWTransclusionDialog );