mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 18:39:52 +00:00
3326eb7240
Change-Id: If0de666db6004f08fbd3a08a0414b66184bb95a0
482 lines
14 KiB
JavaScript
482 lines
14 KiB
JavaScript
/**
|
|
* Specialized layout similar to BookletLayout, but to synchronize the sidebar
|
|
* and content pane of the transclusion dialog
|
|
*
|
|
* Also owns the outline controls.
|
|
*
|
|
* This class has domain knowledge about its contents, for example different
|
|
* behaviors for template vs template parameter elements.
|
|
*
|
|
* @class
|
|
* @extends OO.ui.MenuLayout
|
|
*
|
|
* @constructor
|
|
* @param {Object} [config] Configuration options
|
|
* @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the
|
|
* pages of the booklet.
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout = function VeUiMWTwoPaneTransclusionDialogLayout( config ) {
|
|
// Configuration initialization
|
|
config = config || {};
|
|
|
|
// Parent constructor
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.super.call( this, config );
|
|
|
|
// Properties
|
|
this.currentPageName = null;
|
|
this.pages = {};
|
|
this.ignoreFocus = false;
|
|
this.stackLayout = new OO.ui.StackLayout( {
|
|
continuous: true,
|
|
expanded: this.expanded
|
|
} );
|
|
this.setContentPanel( this.stackLayout );
|
|
this.sidebar = new ve.ui.MWTransclusionOutlineWidget();
|
|
this.autoFocus = true;
|
|
this.outlineVisible = false;
|
|
this.outlined = !!config.outlined;
|
|
if ( this.outlined ) {
|
|
this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
|
|
this.outlinePanel = new OO.ui.PanelLayout( {
|
|
expanded: this.expanded,
|
|
scrollable: true
|
|
} );
|
|
this.setMenuPanel( this.outlinePanel );
|
|
this.outlineVisible = true;
|
|
this.outlineControlsWidget = new ve.ui.MWTransclusionOutlineControlsWidget();
|
|
}
|
|
this.toggleMenu( this.outlined );
|
|
|
|
// Events
|
|
this.sidebar.connect( this, {
|
|
focusPageByName: 'focusPart',
|
|
filterPagesByName: 'onFilterPagesByName',
|
|
selectedTransclusionPartChanged: 'onSelectedTransclusionPartChanged'
|
|
} );
|
|
this.stackLayout.connect( this, {
|
|
set: 'onStackLayoutSet'
|
|
} );
|
|
if ( this.outlined ) {
|
|
this.outlineSelectWidget.connect( this, {
|
|
select: 'onOutlineSelectWidgetSelect'
|
|
} );
|
|
}
|
|
// Event 'focus' does not bubble, but 'focusin' does
|
|
this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
|
|
|
|
// Initialization
|
|
this.$element.addClass( 've-ui-mwTwoPaneTransclusionDialogLayout' );
|
|
this.stackLayout.$element.addClass( 've-ui-mwTwoPaneTransclusionDialogLayout-stackLayout' );
|
|
if ( this.outlined ) {
|
|
this.outlinePanel.$element
|
|
.addClass( 've-ui-mwTwoPaneTransclusionDialogLayout-outlinePanel' )
|
|
.append(
|
|
$( '<div>' ).addClass( 've-ui-mwTwoPaneTransclusionDialogLayout-sidebar-container' )
|
|
.append( this.sidebar.$element ),
|
|
this.outlineControlsWidget.$element
|
|
);
|
|
}
|
|
};
|
|
|
|
/* Setup */
|
|
|
|
OO.inheritClass( ve.ui.MWTwoPaneTransclusionDialogLayout, OO.ui.MenuLayout );
|
|
|
|
/* Events */
|
|
|
|
/**
|
|
* A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the
|
|
* booklet layout.
|
|
*
|
|
* @event set
|
|
* @param {OO.ui.PageLayout} page Current page
|
|
*/
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* @private
|
|
* @param {Object} visibility
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.onFilterPagesByName = function ( visibility ) {
|
|
for ( var pageName in visibility ) {
|
|
var page = this.getPage( pageName );
|
|
if ( page ) {
|
|
page.toggle( visibility[ pageName ] );
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @param {string} partId
|
|
* @param {boolean} internal Used for internal calls to suppress events
|
|
*
|
|
* This method supports using the space bar in a sidebar template header.
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.onSelectedTransclusionPartChanged = function ( partId, internal ) {
|
|
var page = this.getPage( partId );
|
|
if ( page && !internal ) {
|
|
page.scrollElementIntoView();
|
|
}
|
|
|
|
// FIXME: This hack re-implements what OO.ui.SelectWidget.selectItem would do, without firing
|
|
// the "select" event. This will stop working when we disconnect the old sidebar.
|
|
this.outlineSelectWidget.items.forEach( function ( item ) {
|
|
// This repeats what ve.ui.MWTransclusionOutlineWidget.setSelectionByPageName did, but for
|
|
// the old sidebar
|
|
item.setSelected( item.getData() === partId );
|
|
} );
|
|
this.refreshControls();
|
|
};
|
|
|
|
/**
|
|
* Handle stack layout focus.
|
|
*
|
|
* @private
|
|
* @param {jQuery.Event} e Focusin event
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.onStackLayoutFocus = function ( e ) {
|
|
// Find the page that an element was focused within
|
|
var $target = $( e.target ).closest( '.oo-ui-pageLayout' );
|
|
for ( var name in this.pages ) {
|
|
// Check for page match, exclude current page to find only page changes
|
|
if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
|
|
this.setPage( name );
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle stack layout set events.
|
|
*
|
|
* @private
|
|
* @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.onStackLayoutSet = function ( page ) {
|
|
// If everything is unselected, do nothing
|
|
if ( !page ) {
|
|
return;
|
|
}
|
|
// Scroll the selected page into view first
|
|
var promise = page.scrollElementIntoView();
|
|
// Focus the first element on the newly selected panel.
|
|
if ( this.autoFocus && !OO.ui.isMobile() ) {
|
|
promise.done( this.focus.bind( this ) );
|
|
}
|
|
|
|
if ( this.outlined ) {
|
|
var isLastPlaceholder = page instanceof ve.ui.MWTemplatePlaceholderPage &&
|
|
Object.keys( this.pages ).length === 1;
|
|
// TODO: In other cases this is disabled rather than hidden. See T311303
|
|
this.outlineControlsWidget.removeButton.toggle( !isLastPlaceholder );
|
|
}
|
|
|
|
this.sidebar.setSelectionByPageName( page.getName() );
|
|
};
|
|
|
|
/**
|
|
* Focus the first input in the current page.
|
|
*
|
|
* If no page is selected, the first selectable page will be selected.
|
|
* If the focus is already in an element on the current page, nothing will happen.
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.focus = function () {
|
|
var page = this.stackLayout.getCurrentItem();
|
|
|
|
if ( !page ) {
|
|
return;
|
|
}
|
|
// Only change the focus if is not already in the current page
|
|
if ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
|
|
page.focus();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} pageName
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.focusPart = function ( pageName ) {
|
|
if ( pageName.indexOf( '/' ) === -1 ) {
|
|
// FIXME: This is currently needed because the event that adds a new part to the new sidebar
|
|
// is executed later than this here.
|
|
setTimeout( this.sidebar.setSelectionByPageName.bind( this.sidebar, pageName ) );
|
|
this.setPage( pageName );
|
|
// The .setPage() call above does not necessarily call .focus(). This forces it.
|
|
this.focus();
|
|
} else {
|
|
this.setPage( pageName );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle outline widget select events.
|
|
*
|
|
* @private
|
|
* @param {OO.ui.OptionWidget|null} item Selected item
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
|
|
if ( item ) {
|
|
this.setPage( item.getData() );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if booklet has an outline.
|
|
*
|
|
* @return {boolean} Booklet has an outline
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.isOutlined = function () {
|
|
return this.outlined;
|
|
};
|
|
|
|
/**
|
|
* Check if booklet has a visible outline.
|
|
*
|
|
* @return {boolean} Outline is visible
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.isOutlineVisible = function () {
|
|
return this.outlined && this.outlineVisible;
|
|
};
|
|
|
|
/**
|
|
* Hide or show the outline.
|
|
*
|
|
* @param {boolean} [show] Show outline, omit to invert current state
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.toggleOutline = function ( show ) {
|
|
if ( !this.outlined ) {
|
|
return;
|
|
}
|
|
|
|
show = show === undefined ? !this.outlineVisible : !!show;
|
|
this.outlineVisible = show;
|
|
this.toggleMenu( show );
|
|
if ( show ) {
|
|
var booklet = this;
|
|
// HACK: Kill dumb scrollbars when the sidebar stops animating, see T161798.
|
|
// Only necessary when outline controls are present, delay matches transition on
|
|
// `.oo-ui-menuLayout-menu`.
|
|
setTimeout( function () {
|
|
OO.ui.Element.static.reconsiderScrollbars( booklet.outlinePanel.$element[ 0 ] );
|
|
}, OO.ui.theme.getDialogTransitionDuration() );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @return {ve.ui.MWTransclusionOutlineControlsWidget|null}
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.getOutlineControls = function () {
|
|
return this.outlineControlsWidget;
|
|
};
|
|
|
|
/**
|
|
* Get a page by its symbolic name.
|
|
*
|
|
* @param {string} name Symbolic name of page
|
|
* @return {OO.ui.PageLayout|undefined} Page, if found
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.getPage = function ( name ) {
|
|
return this.pages[ name ];
|
|
};
|
|
|
|
/**
|
|
* @return {OO.ui.PageLayout|undefined} Current page, if found
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.getCurrentPage = function () {
|
|
var name = this.getCurrentPageName();
|
|
return name ? this.getPage( name ) : undefined;
|
|
};
|
|
|
|
/**
|
|
* Get the symbolic name of the current page.
|
|
*
|
|
* @return {string|null} Symbolic name of the current page
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.getCurrentPageName = function () {
|
|
return this.currentPageName;
|
|
};
|
|
|
|
/**
|
|
* Add pages to the booklet layout
|
|
*
|
|
* When pages are added with the same names as existing pages, the existing pages will be
|
|
* automatically removed before the new pages are added.
|
|
*
|
|
* @param {OO.ui.PageLayout[]} pages Pages to add
|
|
* @param {number} index Index of the insertion point
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.addPages = function ( pages, index ) {
|
|
var i, name, page,
|
|
stackLayoutPages = this.stackLayout.getItems();
|
|
|
|
// Remove pages with same names
|
|
var remove = [];
|
|
for ( i = 0; i < pages.length; i++ ) {
|
|
page = pages[ i ];
|
|
name = page.getName();
|
|
|
|
if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
|
|
// Correct the insertion index
|
|
var currentIndex = stackLayoutPages.indexOf( this.pages[ name ] );
|
|
if ( currentIndex !== -1 && currentIndex + 1 < index ) {
|
|
index--;
|
|
}
|
|
remove.push( this.pages[ name ] );
|
|
}
|
|
}
|
|
if ( remove.length ) {
|
|
this.removePages( remove );
|
|
}
|
|
|
|
// Add new pages
|
|
var items = [];
|
|
for ( i = 0; i < pages.length; i++ ) {
|
|
page = pages[ i ];
|
|
name = page.getName();
|
|
this.pages[ page.getName() ] = page;
|
|
if ( this.outlined ) {
|
|
var item = new OO.ui.OutlineOptionWidget( { data: name } );
|
|
page.setOutlineItem( item );
|
|
items.push( item );
|
|
}
|
|
}
|
|
|
|
if ( this.outlined ) {
|
|
this.outlineSelectWidget.addItems( items, index );
|
|
// It's impossible to lose a selection here. Selecting something else is business logic.
|
|
}
|
|
this.stackLayout.addItems( pages, index );
|
|
};
|
|
|
|
/**
|
|
* Remove the specified pages from the booklet layout.
|
|
*
|
|
* To remove all pages from the booklet, you may wish to use the #clearPages method instead.
|
|
*
|
|
* @param {OO.ui.PageLayout[]} pages An array of pages to remove
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.removePages = function ( pages ) {
|
|
var itemsToRemove = [];
|
|
|
|
for ( var i = 0; i < pages.length; i++ ) {
|
|
var page = pages[ i ],
|
|
name = page.getName();
|
|
delete this.pages[ name ];
|
|
if ( this.outlined ) {
|
|
itemsToRemove.push( this.outlineSelectWidget.findItemFromData( name ) );
|
|
page.setOutlineItem( null );
|
|
}
|
|
// If the current page is removed, clear currentPageName
|
|
if ( this.currentPageName === name ) {
|
|
this.currentPageName = null;
|
|
}
|
|
}
|
|
if ( itemsToRemove.length ) {
|
|
this.outlineSelectWidget.removeItems( itemsToRemove );
|
|
// We might loose the selection here, but what to select instead is business logic.
|
|
}
|
|
this.stackLayout.removeItems( pages );
|
|
};
|
|
|
|
/**
|
|
* Clear all pages from the booklet layout.
|
|
*
|
|
* To remove only a subset of pages from the booklet, use the #removePages method.
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.clearPages = function () {
|
|
var pages = this.stackLayout.getItems();
|
|
|
|
this.pages = {};
|
|
this.currentPageName = null;
|
|
if ( this.outlined ) {
|
|
this.outlineSelectWidget.clearItems();
|
|
for ( var i = 0; i < pages.length; i++ ) {
|
|
pages[ i ].setOutlineItem( null );
|
|
}
|
|
}
|
|
this.sidebar.clear();
|
|
this.stackLayout.clearItems();
|
|
};
|
|
|
|
/**
|
|
* Set the current page by symbolic name.
|
|
*
|
|
* @fires set
|
|
* @param {string} name Symbolic name of page
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.setPage = function ( name ) {
|
|
var page = this.pages[ name ];
|
|
if ( !page || name === this.currentPageName ) {
|
|
return;
|
|
}
|
|
|
|
var previousPage = this.currentPageName ? this.pages[ this.currentPageName ] : null;
|
|
this.currentPageName = name;
|
|
|
|
if ( this.outlined ) {
|
|
var selectedItem = this.outlineSelectWidget.findSelectedItem();
|
|
if ( !selectedItem || selectedItem.getData() !== name ) {
|
|
// Warning! This triggers a "select" event and the .onOutlineSelectWidgetSelect()
|
|
// handler, which calls .setPage() a second time. Make sure .currentPageName is set to
|
|
// break this loop.
|
|
this.outlineSelectWidget.selectItemByData( name );
|
|
}
|
|
}
|
|
|
|
if ( previousPage ) {
|
|
previousPage.setActive( false );
|
|
// Blur anything focused if the next page doesn't have anything focusable.
|
|
// This is not needed if the next page has something focusable (because once it is
|
|
// focused this blur happens automatically).
|
|
if ( !OO.ui.isMobile() &&
|
|
OO.ui.findFocusable( page.$element ).length !== 0
|
|
) {
|
|
var $focused = previousPage.$element.find( ':focus' );
|
|
if ( $focused.length ) {
|
|
$focused[ 0 ].blur();
|
|
}
|
|
}
|
|
}
|
|
page.setActive( true );
|
|
this.stackLayout.setItem( page );
|
|
|
|
this.refreshControls();
|
|
|
|
this.emit( 'set', page );
|
|
};
|
|
|
|
/**
|
|
* Refresh controls
|
|
*
|
|
*/
|
|
ve.ui.MWTwoPaneTransclusionDialogLayout.prototype.refreshControls = function () {
|
|
var pages = this.stackLayout.getItems(),
|
|
page = this.getCurrentPage(),
|
|
index = pages.indexOf( page ),
|
|
isParameter = page instanceof ve.ui.MWParameterPage,
|
|
canMoveUp, canMoveDown = false,
|
|
canBeDeleted = !isParameter;
|
|
|
|
/* check if this is the first element and no parameter */
|
|
canMoveUp = !isParameter && index > 0;
|
|
|
|
/* check if this is the last element and no parameter */
|
|
if ( !isParameter ) {
|
|
for ( var i = index + 1; i < pages.length; i++ ) {
|
|
if ( !( pages[ i ] instanceof ve.ui.MWParameterPage || pages[ i ] instanceof ve.ui.MWAddParameterPage ) ) {
|
|
canMoveDown = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.outlineControlsWidget.setButtonsEnabled( {
|
|
canMoveUp: canMoveUp,
|
|
canMoveDown: canMoveDown,
|
|
canBeDeleted: canBeDeleted
|
|
} );
|
|
|
|
};
|