mediawiki-extensions-Visual.../modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js

329 lines
9.9 KiB
JavaScript
Raw Normal View History

/*!
* VisualEditor UserInterface MediaWiki MWReferenceDialog class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Dialog for editing MediaWiki references.
*
* @class
* @extends ve.ui.MWDialog
*
* @constructor
* @param {ve.ui.Surface} surface
* @param {Object} [config] Config options
*/
ve.ui.MWReferenceDialog = function VeUiMWReferenceDialog( surface, config ) {
// Parent constructor
ve.ui.MWDialog.call( this, surface, config );
// Properties
this.ref = null;
};
/* Inheritance */
ve.inheritClass( ve.ui.MWReferenceDialog, ve.ui.MWDialog );
/* Static Properties */
ve.ui.MWReferenceDialog.static.name = 'reference';
ve.ui.MWReferenceDialog.static.titleMessage = 'visualeditor-dialog-reference-title';
ve.ui.MWReferenceDialog.static.icon = 'reference';
ve.ui.MWReferenceDialog.static.toolbarGroups = [
{ 'include': [ 'undo', 'redo' ] },
{ 'include': [ 'bold', 'italic', 'link', 'clear' ] },
{ 'include': [ 'number', 'bullet', 'outdent', 'indent' ] },
{ 'include': '*', 'exclude': [ { 'group': 'format' }, 'reference', 'referenceList' ] }
];
ve.ui.MWReferenceDialog.static.surfaceCommands = [
'undo', 'redo', 'bold', 'italic', 'link', 'clear'
];
/* Methods */
/**
* @inheritdoc
*/
ve.ui.MWReferenceDialog.prototype.initialize = function () {
// Parent method
ve.ui.MWDialog.prototype.initialize.call( this );
// Properties
this.panels = new ve.ui.StackPanelLayout( { '$$': this.frame.$$ } );
this.editPanel = new ve.ui.PanelLayout( {
'$$': this.frame.$$, 'scrollable': true, 'padded': true
} );
this.searchPanel = new ve.ui.PanelLayout( { '$$': this.frame.$$ } );
this.applyButton = new ve.ui.ButtonWidget( {
'$$': this.frame.$$,
'label': ve.msg( 'visualeditor-dialog-action-apply' ),
'flags': ['primary']
} );
this.insertButton = new ve.ui.ButtonWidget( {
'$$': this.frame.$$,
'label': ve.msg( 'visualeditor-dialog-reference-insert-button' ),
'flags': ['constructive']
} );
this.selectButton = new ve.ui.ButtonWidget( {
'$$': this.frame.$$,
'label': ve.msg ( 'visualeditor-dialog-reference-useexisting-label' )
} );
this.backButton = new ve.ui.ButtonWidget( {
'$$': this.frame.$$,
'label': ve.msg( 'visualeditor-dialog-action-goback' )
} );
this.contentFieldset = new ve.ui.FieldsetLayout( { '$$': this.frame.$$ } );
this.optionsFieldset = new ve.ui.FieldsetLayout( {
'$$': this.frame.$$,
'label': ve.msg( 'visualeditor-dialog-reference-options-section' ),
'icon': 'settings'
} );
// TODO: Use a drop-down or something, and populate with existing groups instead of free-text
this.referenceGroupInput = new ve.ui.TextInputWidget( { '$$': this.frame.$$ } );
this.referenceGroupLabel = new ve.ui.InputLabelWidget( {
'$$': this.frame.$$,
'input': this.referenceGroupInput,
'label': ve.msg( 'visualeditor-dialog-reference-options-group-label' )
} );
this.search = new ve.ui.MWReferenceSearchWidget( this.surface, { '$$': this.frame.$$ } );
// Events
this.applyButton.connect( this, { 'click': [ 'close', 'apply' ] } );
this.insertButton.connect( this, { 'click': [ 'close', 'insert' ] } );
this.selectButton.connect( this, { 'click': function () {
this.backButton.$.show();
this.insertButton.$.hide();
this.selectButton.$.hide();
this.panels.showItem( this.searchPanel );
this.search.getQuery().$input.focus().select();
} } );
this.backButton.connect( this, { 'click': function () {
this.backButton.$.hide();
this.insertButton.$.show();
this.selectButton.$.show();
this.panels.showItem( this.editPanel );
this.editPanel.$.find( '.ve-ce-documentNode' ).focus();
} } );
this.search.connect( this, { 'select': 'onSearchSelect' } );
// Initialization
this.panels.addItems( [ this.editPanel, this.searchPanel ] );
this.editPanel.$.append( this.contentFieldset.$, this.optionsFieldset.$ );
this.optionsFieldset.$.append( this.referenceGroupLabel.$, this.referenceGroupInput.$ );
this.searchPanel.$.append( this.search.$ );
this.$body.append( this.panels.$ );
this.$foot.append(
this.applyButton.$,
this.insertButton.$,
this.selectButton.$,
this.backButton.$
);
};
/**
* Handle search select events.
*
* @param {Object|null} item Reference attributes or null if no item is selected
*/
ve.ui.MWReferenceDialog.prototype.onSearchSelect = function ( item ) {
if ( item ) {
this.useReference( item );
this.close( 'insert' );
}
};
/**
* @inheritdoc
*/
ve.ui.MWReferenceDialog.prototype.onOpen = function () {
var ref,
focusedNode = this.surface.getView().getFocusedNode();
// Parent method
ve.ui.MWDialog.prototype.onOpen.call( this );
if ( focusedNode instanceof ve.ce.MWReferenceNode ) {
ref = focusedNode.getModel().getAttributes();
this.applyButton.$.show();
this.insertButton.$.hide();
this.selectButton.$.hide();
} else {
this.applyButton.$.hide();
this.insertButton.$.show();
this.selectButton.$.show();
}
this.backButton.$.hide();
this.panels.showItem( this.editPanel );
this.useReference( ref );
this.search.buildIndex();
this.selectButton.setDisabled( !this.search.getResults().getItems().length );
};
/**
* @inheritdoc
*/
ve.ui.MWReferenceDialog.prototype.onClose = function ( action ) {
var i, len, txs, item, data, group, refGroup, listGroup, keyIndex, refNodes,
surfaceModel = this.surface.getModel(),
// Store the original selection browsers may reset it after
// the first model change.
selection = surfaceModel.getSelection().clone(),
doc = surfaceModel.getDocument(),
internalList = doc.getInternalList();
// Parent method
ve.ui.MWDialog.prototype.onClose.call( this, action );
if ( action === 'insert' || action === 'apply' ) {
data = this.referenceSurface.getContent();
refGroup = this.referenceGroupInput.getValue();
listGroup = 'mwReference/' + refGroup;
// Internal item changes
if ( this.ref ) {
// Group/key has changed
if ( this.ref.listGroup !== listGroup ) {
// Get all reference nodes with the same group and key
group = internalList.getNodeGroup( this.ref.listGroup );
refNodes = group.keyedNodes[this.ref.listKey] ?
group.keyedNodes[this.ref.listKey].slice() :
[ group.firstNodes[this.ref.listIndex] ];
// Check for name collision when moving items between groups
keyIndex = internalList.getKeyIndex( this.ref.listGroup, this.ref.listKey );
if ( keyIndex !== undefined ) {
// Resolve name collision by generating a new list key
Prevent naming collisions when generating unique reference names Simply generating ':3' as the "unique" name for the 4th reference doesn't work. Even if getUniqueListKey() had been used, that only checks for conflicts with names that have already been encountered (i.e. occur in <ref> tags that precede the current one), not for conflicts with names that first occur further down in the document. The solution is to generate names at serialization time, when we have full knowledge of which names are in use. Internally, we use 'literal/<name>' for names that literally appeared in the source, and 'auto/<number>' for unnamed references. Then at serialization time, we translate 'auto/<number>' to 'literal/:<number>' if needed (i.e. if the reference was reused). ve.dm.MWReferenceNode.js: * toDataElement() ** Prefix listKey with literal/ or auto/ as appropriate * toDomElements() ** Map auto/ listKeys to unique names ** Don't try to unset the name if not present (was unsetting a property that didn't exist anyway) ve.dm.InternalList.js: * Remove now-unused isUniqueListKey() * Rewrite getUniqueListKey() ** Make prefix configurable ** Take previously generated unique keys into account ** Map the same old key (auto/N) to the same generated key (literal/:M) * Add getNextUniqueNumber() as a source for auto/N numbers: previously used the length of the itemHtmlQueue, but that only works during conversion, not from the UI dialog ve.ui.MWReferenceDialog.js: * For new references or conflicting names, generate an auto/N key and let toDomElements() deal with actually mapping that to name ve.dm.InternalList.test.js: * Rename listKeys to new style * Split the test case into two groups so we can test multi-group cases * Add tests for getUniqueListKey() ve.dm.mwExample.js: * Rename things to new style * Modify the test case so it attempts to trigger bug 54341 Bug: 54341 Change-Id: I726fb83e6fb66ffec643d996768a854ec9474b3d
2013-09-19 22:57:08 +00:00
this.ref.listKey = 'auto/' + internalList.getNextUniqueNumber();
}
// Update the group name of all references nodes with the same group and key
txs = [];
for ( i = 0, len = refNodes.length; i < len; i++ ) {
// HACK: Removing and re-inserting nodes to/from the internal list is done
// because internal list doesn't yet support attribute changes
refNodes[i].removeFromInternalList();
txs.push( ve.dm.Transaction.newFromAttributeChanges(
doc,
refNodes[i].getOuterRange().start,
{ 'refGroup': refGroup, 'listGroup': listGroup }
) );
}
surfaceModel.change( txs );
// HACK: Same as above, internal list issues
for ( i = 0, len = refNodes.length; i < len; i++ ) {
refNodes[i].addToInternalList();
}
this.ref.listGroup = listGroup;
this.ref.refGroup = refGroup;
}
// Update internal node content
surfaceModel.change(
ve.dm.Transaction.newFromNodeReplacement(
doc, internalList.getItemNode( this.ref.listIndex ), data
)
);
}
// Content changes
if ( action === 'insert' ) {
if ( !this.ref ) {
listGroup = 'mwReference/' + refGroup;
// Create new internal item
this.ref = {
Prevent naming collisions when generating unique reference names Simply generating ':3' as the "unique" name for the 4th reference doesn't work. Even if getUniqueListKey() had been used, that only checks for conflicts with names that have already been encountered (i.e. occur in <ref> tags that precede the current one), not for conflicts with names that first occur further down in the document. The solution is to generate names at serialization time, when we have full knowledge of which names are in use. Internally, we use 'literal/<name>' for names that literally appeared in the source, and 'auto/<number>' for unnamed references. Then at serialization time, we translate 'auto/<number>' to 'literal/:<number>' if needed (i.e. if the reference was reused). ve.dm.MWReferenceNode.js: * toDataElement() ** Prefix listKey with literal/ or auto/ as appropriate * toDomElements() ** Map auto/ listKeys to unique names ** Don't try to unset the name if not present (was unsetting a property that didn't exist anyway) ve.dm.InternalList.js: * Remove now-unused isUniqueListKey() * Rewrite getUniqueListKey() ** Make prefix configurable ** Take previously generated unique keys into account ** Map the same old key (auto/N) to the same generated key (literal/:M) * Add getNextUniqueNumber() as a source for auto/N numbers: previously used the length of the itemHtmlQueue, but that only works during conversion, not from the UI dialog ve.ui.MWReferenceDialog.js: * For new references or conflicting names, generate an auto/N key and let toDomElements() deal with actually mapping that to name ve.dm.InternalList.test.js: * Rename listKeys to new style * Split the test case into two groups so we can test multi-group cases * Add tests for getUniqueListKey() ve.dm.mwExample.js: * Rename things to new style * Modify the test case so it attempts to trigger bug 54341 Bug: 54341 Change-Id: I726fb83e6fb66ffec643d996768a854ec9474b3d
2013-09-19 22:57:08 +00:00
'listKey': 'auto/' + internalList.getNextUniqueNumber(),
'listGroup': listGroup,
'refGroup': refGroup
};
item = internalList.getItemInsertion( this.ref.listGroup, this.ref.listKey, data );
surfaceModel.change( item.transaction );
this.ref.listIndex = item.index;
}
// Add reference at cursor
surfaceModel.getFragment( selection ).collapseRangeToEnd().insertContent( [
{ 'type': 'mwReference', 'attributes': this.ref }, { 'type': '/mwReference' }
] );
}
}
this.referenceSurface.destroy();
this.referenceSurface = null;
this.ref = null;
};
/**
* Work on a specific reference.
*
* @param {Object} [ref] Reference attributes, omit to work on a new reference
* @chainable
*/
ve.ui.MWReferenceDialog.prototype.useReference = function ( ref ) {
var data, refGroup,
doc = this.surface.getModel().getDocument();
if ( ref ) {
// Use an existing reference
this.ref = {
'listKey': ref.listKey,
'listGroup': ref.listGroup,
'refGroup': ref.refGroup,
'listIndex': ref.listIndex
};
data = doc.getData( doc.getInternalList().getItemNode( ref.listIndex ).getRange(), true );
refGroup = ref.refGroup;
} else {
// Create a new reference
this.ref = null;
data = [
{ 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
{ 'type': '/paragraph' }
];
refGroup = '';
}
// Cleanup
if ( this.referenceSurface ) {
this.referenceSurface.destroy();
}
// Properties
this.referenceSurface = new ve.ui.SurfaceWidget(
new ve.dm.ElementLinearData( doc.getStore(), data ),
{
'$$': this.frame.$$,
'tools': this.constructor.static.toolbarGroups,
'commands': this.constructor.static.surfaceCommands
}
);
// Event handlers
this.referenceSurface.getSurface().getModel().connect( this, { 'change': 'onSurfaceChange' } );
// Initialization
this.referenceGroupInput.setValue( refGroup );
this.contentFieldset.$.append( this.referenceSurface.$ );
this.referenceSurface.initialize();
return this;
};
/**
* Handle reference surface change events
*/
ve.ui.MWReferenceDialog.prototype.onSurfaceChange = function () {
var data = this.referenceSurface.getContent(),
// TODO: Check for other types of empty, e.g. only whitespace?
disabled = data.length <= 2;
this.insertButton.setDisabled( disabled );
this.applyButton.setDisabled( disabled );
};
/* Registration */
ve.ui.dialogFactory.register( ve.ui.MWReferenceDialog );