Reference name and group editing

Objective:

* Allow editing reference groups and names in the reference dialog

Bonus:

* Modify attribute transaction builder to support multiple attribute
  changes in a single transaction

Changes:

ve.ui.MWReferenceDialog.js
* Load ref name and group from model
* Save ref name and group, if changed, to model

ve.ui.ListAction.js, ve.ui.Transaction.test.js, ve.ce.ResizableNode.js
* Update use of newFromAttributeChange to newFromAttributeChanges

ve.dm.SurfaceFragment.test.js
* Add test for new changeAttributes method

ve.dm.InternalList.js
* Missing new line at end of file

ve.dm.Transaction.js
* Change newFromAttributeChange to accept an list of attribute changes and
  produce a single transaction that applies one or more attribute changes
  at once

ve.dm.SurfaceFragment.js
* Fix bug in getCoveredNodes where the wrong mode name was being used
* Add changeAttributes method, which applies attributes to all covered
  nodes and allows filtering of which types of nodes the attributes are
  applied to

ve.dm.MWReferenceNode.js
* Actually write key and group back to DOM
* Separate onRoot functionality into addToInternalList so it can be called
  separately (similarly onUnroot/removeFromInternalList)

ve.ce.MWReferenceListNode.js
* Clone internal item CE node before appending to avoid rendering bug.

*.php
* Add links to messages and sort them

Change-Id: Ic4121e4fcfc09265d5863af6f078cdeb77926c8e
This commit is contained in:
Trevor Parscal 2013-06-14 12:07:55 -07:00
parent 3e1e544e48
commit fc8c46dd74
12 changed files with 219 additions and 77 deletions

View file

@ -55,14 +55,14 @@ $messages['en'] = array(
'visualeditor-dialog-reference-options-group-label' => 'Use this group',
'visualeditor-dialog-reference-title' => 'Reference',
'visualeditor-dialog-media-title' => 'Media settings',
'visualeditor-dialog-transclusion-title' => 'Transclusion',
'visualeditor-dialog-transclusion-add-param' => 'Add parameter',
'visualeditor-dialog-transclusion-content' => 'Content',
'visualeditor-dialog-transclusion-options' => 'Options',
'visualeditor-dialog-transclusion-remove-content' => 'Remove content',
'visualeditor-dialog-transclusion-remove-template' => 'Remove template',
'visualeditor-dialog-transclusion-remove-param' => 'Remove parameter',
'visualeditor-dialog-transclusion-add-param' => 'Add parameter',
'visualeditor-dialog-transclusion-param-name' => 'Parameter name',
'visualeditor-dialog-transclusion-remove-content' => 'Remove content',
'visualeditor-dialog-transclusion-remove-param' => 'Remove parameter',
'visualeditor-dialog-transclusion-remove-template' => 'Remove template',
'visualeditor-dialog-transclusion-title' => 'Transclusion',
'visualeditor-dialogbutton-media-tooltip' => 'Media',
'visualeditor-dialogbutton-meta-tooltip' => 'Page settings',
'visualeditor-dialogbutton-reference-tooltip' => 'Reference',
@ -220,20 +220,24 @@ See also:
{{Identical|Language}}',
'visualeditor-dialog-meta-title' => 'MetaData dialog title text.
{{Identical|Page settings}}',
'visualeditor-dialog-reference-content-section' => 'Label for the reference content sub-section',
'visualeditor-dialog-reference-options-section' => 'Label for the reference options sub-section',
'visualeditor-dialog-reference-options-name-label' => 'Label for the reference name input',
'visualeditor-dialog-reference-options-group-label' => 'Label for the reference group input',
'visualeditor-dialog-reference-title' => '{{Identical|Reference}}',
'visualeditor-dialog-transclusion-title' => '{{Identical|Transclusion}}',
'visualeditor-dialog-transclusion-add-param' => 'Label for button that adds parameter a parameter to a template.
{{Identical|Add parameter}}',
'visualeditor-dialog-transclusion-content' => 'Label for editor of content between transclusion parts.
{{Identical|Content}}',
'visualeditor-dialog-transclusion-options' => 'Label for section with options for templates, content or parameters.
{{Identical|Options}}',
'visualeditor-dialog-transclusion-remove-content' => 'Label for button that removes content between transclusion parts',
'visualeditor-dialog-transclusion-remove-template' => 'Label for button that removes a template from a transclusion.
{{Identical|Remove template}}',
'visualeditor-dialog-transclusion-remove-param' => 'Label for button that removes a parameter from a template',
'visualeditor-dialog-transclusion-add-param' => 'Label for button that adds parameter a parameter to a template.
{{Identical|Add parameter}}',
'visualeditor-dialog-transclusion-param-name' => 'Placeholder text label for an input for adding a parameter to a template.
{{Identical|Parameter name}}',
'visualeditor-dialog-transclusion-remove-content' => 'Label for button that removes content between transclusion parts',
'visualeditor-dialog-transclusion-remove-param' => 'Label for button that removes a parameter from a template',
'visualeditor-dialog-transclusion-remove-template' => 'Label for button that removes a template from a transclusion.
{{Identical|Remove template}}',
'visualeditor-dialog-transclusion-title' => '{{Identical|Transclusion}}',
'visualeditor-dialogbutton-media-tooltip' => '{{Identical|Media}}',
'visualeditor-dialogbutton-meta-tooltip' => '{{Identical|Page Settings}}',
'visualeditor-dialogbutton-reference-tooltip' => '{{Identical|Reference}}',

View file

@ -585,20 +585,21 @@ $wgResourceModules += array(
'messages' => array(
// VE messages needed by code that is only in experimental mode
'visualeditor-dialog-reference-content-section',
'visualeditor-dialog-reference-options-section',
'visualeditor-dialog-reference-options-name-label',
'visualeditor-dialog-reference-options-group-label',
'visualeditor-dialog-reference-options-name-label',
'visualeditor-dialog-reference-options-section',
'visualeditor-dialog-reference-title',
'visualeditor-dialog-transclusion-title',
'visualeditor-dialogbutton-reference-tooltip',
'visualeditor-dialogbutton-transclusion-tooltip',
'visualeditor-dialog-transclusion-add-param',
'visualeditor-dialog-transclusion-content',
'visualeditor-dialog-transclusion-options',
'visualeditor-dialog-transclusion-remove-content',
'visualeditor-dialog-transclusion-remove-template',
'visualeditor-dialog-transclusion-remove-param',
'visualeditor-dialog-transclusion-add-param',
'visualeditor-dialog-transclusion-param-name',
'visualeditor-dialog-transclusion-remove-content',
'visualeditor-dialog-transclusion-remove-param',
'visualeditor-dialog-transclusion-remove-template',
'visualeditor-dialog-transclusion-title',
'visualeditor-dialog-transclusion-wikitext-label',
'visualeditor-dialogbutton-reference-tooltip',
'visualeditor-dialogbutton-transclusion-tooltip',
),
),
'ext.visualEditor.icons-raster' => $wgVisualEditorResourceTemplate + array(

View file

@ -129,7 +129,7 @@ ve.ce.MWReferenceListNode.prototype.update = function () {
// HACK: ProtectedNode crashes when dealing with an unattached node
this.attachedItems.push( itemNode );
itemNode.attach( this );
$li.append( $( '<span class="reference-text">' ).html( itemNode.$.show() ) );
$li.append( $( '<span class="reference-text">' ).html( itemNode.$.clone().show() ) );
this.$reflist.append( $li );
}
} // TODO: Show a placeholder for an empty reference list in the 'else' section

View file

@ -245,7 +245,8 @@ ve.ce.ResizableNode.prototype.onDocumentMouseUp = function () {
height = this.$resizeHandles.outerHeight(),
surfaceModel = this.getRoot().getSurface().getModel(),
documentModel = surfaceModel.getDocument(),
selection = surfaceModel.getSelection();
selection = surfaceModel.getSelection(),
attrChanges = {};
this.$resizeHandles.removeClass( 've-ce-resizableNode-handles-resizing' );
$( this.getElementDocument() ).off( '.ve-ce-resizableNode' );
@ -259,8 +260,6 @@ ve.ce.ResizableNode.prototype.onDocumentMouseUp = function () {
// Allow resize to occur before re-rendering
setTimeout( ve.bind( function () {
var txs = [];
if ( transition ) {
// Prevent further transitioning
this.$resizable.removeClass( 've-ce-resizableNode-transitioning' );
@ -268,17 +267,16 @@ ve.ce.ResizableNode.prototype.onDocumentMouseUp = function () {
// Apply changes to the model
if ( this.model.getAttribute( 'width' ) !== width ) {
txs.push( ve.dm.Transaction.newFromAttributeChange(
documentModel, offset, 'width', width
) );
attrChanges.width = width;
}
if ( this.model.getAttribute( 'height' ) !== height ) {
txs.push( ve.dm.Transaction.newFromAttributeChange(
documentModel, offset, 'height', height
) );
attrChanges.height = height;
}
if ( txs.length > 0 ) {
surfaceModel.change( txs, selection );
if ( !ve.isEmptyObject( attrChanges ) ) {
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChanges( documentModel, offset, attrChanges ),
selection
);
}
this.emit( 'resize' );

View file

@ -110,6 +110,19 @@ ve.dm.MWReferenceNode.static.toDomElements = function ( dataElement, doc, conver
ve.setProp( mwAttr, 'body', 'html', itemNodeHtml );
}
// Set or clear key
if ( dataElement.attributes.listKey !== null ) {
ve.setProp( mwAttr, 'attrs', 'name', dataElement.attributes.listKey );
} else if ( mwAttr.attrs ) {
delete mwAttr.attrs.listKey;
}
// Set or clear group
if ( dataElement.attributes.refGroup !== '' ) {
ve.setProp( mwAttr, 'attrs', 'group', dataElement.attributes.refGroup );
} else if ( mwAttr.attrs ) {
delete mwAttr.attrs.refGroup;
}
span.setAttribute( 'data-mw', JSON.stringify( mwAttr ) );
return [ span ];
@ -135,6 +148,22 @@ ve.dm.MWReferenceNode.prototype.getInternalItem = function () {
* @method
*/
ve.dm.MWReferenceNode.prototype.onRoot = function () {
this.addToInternalList();
};
/**
* Handle the node being detatched from the root
* @method
*/
ve.dm.MWReferenceNode.prototype.onUnroot = function () {
this.removeFromInternalList();
};
/**
* Register the node with the internal list
* @method
*/
ve.dm.MWReferenceNode.prototype.addToInternalList = function () {
if ( this.getRoot() === this.getDocument().getDocumentNode() ) {
this.getDocument().getInternalList().addNode(
this.element.attributes.listGroup,
@ -146,10 +175,10 @@ ve.dm.MWReferenceNode.prototype.onRoot = function () {
};
/**
* Handle the node being detatched from the root
* Unregister the node from the internal list
* @method
*/
ve.dm.MWReferenceNode.prototype.onUnroot = function () {
ve.dm.MWReferenceNode.prototype.removeFromInternalList = function () {
this.getDocument().getInternalList().removeNode(
this.element.attributes.listGroup,
this.element.attributes.listKey,

View file

@ -401,7 +401,7 @@ ve.dm.SurfaceFragment.prototype.getCoveredNodes = function () {
if ( !this.surface ) {
return [];
}
return this.document.selectNodes( this.getRange(), 'coveredNodes' );
return this.document.selectNodes( this.getRange(), 'covered' );
};
/**
@ -449,6 +449,48 @@ ve.dm.SurfaceFragment.prototype.select = function () {
return this;
};
/**
* Change one or more attributes on covered nodes.
*
* @method
* @param {Object} attr List of attributes to change, use undefined to remove an attribute
* @param {string} [type] Node type to restrict changes to
* @chainable
*/
ve.dm.SurfaceFragment.prototype.changeAttributes = function ( attr, type ) {
// Handle null fragment
if ( !this.surface ) {
return this;
}
var i, len, result,
txs = [],
covered = this.getCoveredNodes();
for ( i = 0, len = covered.length; i < len; i++ ) {
result = covered[i];
if (
// Non-wrapped nodes have no attributes
!result.node.isWrapped() ||
// Filtering by node type
( type && result.node.getType() !== type ) ||
// Ignore zero-length results
( result.range && result.range.isCollapsed() )
) {
continue;
}
txs.push(
ve.dm.Transaction.newFromAttributeChanges(
this.document, result.nodeOuterRange.start, attr
)
);
}
if ( txs.length ) {
this.surface.change( txs );
}
return this;
};
/**
* Apply an annotation to content in the fragment.
*

View file

@ -179,20 +179,21 @@ ve.dm.Transaction.newFromNodeReplacement = function ( doc, nodeOrRange, newData
};
/**
* Generate a transaction that changes an attribute.
* Generate a transaction that changes one or more attributes.
*
* @static
* @method
* @param {ve.dm.Document} doc Document to create transaction for
* @param {number} offset Offset of element
* @param {string} key Attribute name
* @param {Mixed} value New value, or undefined to remove the attribute
* @param {Object.<string,Mixed>} attr List of attribute key and value pairs, use undefined value
* to remove an attribute
* @returns {ve.dm.Transaction} Transaction that changes an element
* @throws {Error} Cannot set attributes to non-element data
* @throws {Error} Cannot set attributes on closing element
*/
ve.dm.Transaction.newFromAttributeChange = function ( doc, offset, key, value ) {
var tx = new ve.dm.Transaction(),
ve.dm.Transaction.newFromAttributeChanges = function ( doc, offset, attr ) {
var key,
tx = new ve.dm.Transaction(),
data = doc.getData();
// Verify element exists at offset
if ( data[offset].type === undefined ) {
@ -205,9 +206,11 @@ ve.dm.Transaction.newFromAttributeChange = function ( doc, offset, key, value )
// Retain up to element
tx.pushRetain( offset );
// Change attribute
tx.pushReplaceElementAttribute(
key, 'attributes' in data[offset] ? data[offset].attributes[key] : undefined, value
);
for ( key in attr ) {
tx.pushReplaceElementAttribute(
key, 'attributes' in data[offset] ? data[offset].attributes[key] : undefined, attr[key]
);
}
// Retain to end of document
tx.pushRetain( data.length - offset );
return tx;

View file

@ -261,4 +261,4 @@ QUnit.test( 'getItemInsertion', 4, function ( assert ) {
insertion = internalList.getItemInsertion( 'mwReference/', 'foo', [] );
assert.equal( insertion.index, index, 'Insertion with duplicate key reuses old index' );
assert.equal( insertion.transaction, null, 'Insertion with duplicate key has null transaction' );
} );
} );

View file

@ -224,6 +224,18 @@ QUnit.test( 'insertContent', 4, function ( assert ) {
);
} );
QUnit.test( 'changeAttributes', 1, function ( assert ) {
var doc = ve.dm.example.createExampleDocument(),
surface = new ve.dm.Surface( doc ),
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 0, 5 ) );
fragment.changeAttributes( { 'level': 3 } );
assert.deepEqual(
doc.getData( new ve.Range( 0, 1 ) ),
[ { 'type': 'heading', 'attributes': { 'level': 3 } } ],
'changing attributes affects covered nodes'
);
} );
QUnit.test( 'wrapNodes/unwrapNodes', 10, function ( assert ) {
var doc = ve.dm.example.createExampleDocument(),
originalDoc = ve.dm.example.createExampleDocument(),

View file

@ -689,12 +689,11 @@ QUnit.test( 'newFromNodeReplacement', function ( assert ) {
QUnit.expect( ve.getObjectKeys( cases ).length );
runConstructorTests( assert, ve.dm.Transaction.newFromNodeReplacement, cases );
} );
QUnit.test( 'newFromAttributeChange', function ( assert ) {
QUnit.test( 'newFromAttributeChanges', function ( assert ) {
var doc = ve.dm.example.createExampleDocument(),
cases = {
'first element': {
'args': [doc, 0, 'level', 2],
'args': [doc, 0, { 'level': 2 }],
'ops': [
{
'type': 'attribute',
@ -706,7 +705,7 @@ QUnit.test( 'newFromAttributeChange', function ( assert ) {
]
},
'middle element': {
'args': [doc, 17, 'style', 'number'],
'args': [doc, 17, { 'style': 'number'} ],
'ops': [
{ 'type': 'retain', 'length': 17 },
{
@ -718,17 +717,36 @@ QUnit.test( 'newFromAttributeChange', function ( assert ) {
{ 'type': 'retain', 'length': 44 }
]
},
'multiple attributes': {
'args': [doc, 17, { 'style': 'number', 'level': 1 } ],
'ops': [
{ 'type': 'retain', 'length': 17 },
{
'type': 'attribute',
'key': 'style',
'from': 'bullet',
'to': 'number'
},
{
'type': 'attribute',
'key': 'level',
'from': undefined,
'to': 1
},
{ 'type': 'retain', 'length': 44 }
]
},
'non-element': {
'args': [doc, 1, 'level', 2],
'args': [doc, 1, { 'level': 2 }],
'exception': Error
},
'closing element': {
'args': [doc, 4, 'level', 2],
'args': [doc, 4, { 'level': 2 }],
'exception': Error
}
};
QUnit.expect( ve.getObjectKeys( cases ).length );
runConstructorTests( assert, ve.dm.Transaction.newFromAttributeChange, cases );
runConstructorTests( assert, ve.dm.Transaction.newFromAttributeChanges, cases );
} );
QUnit.test( 'newFromAnnotation', function ( assert ) {

View file

@ -56,8 +56,8 @@ ve.ui.ListAction.prototype.wrap = function ( style ) {
if ( group.grandparent !== previousList ) {
// Change the list style
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChange(
documentModel, group.grandparent.getOffset(), 'style', style
ve.dm.Transaction.newFromAttributeChanges(
documentModel, group.grandparent.getOffset(), { 'style': style }
),
selection
);

View file

@ -106,7 +106,7 @@ ve.ui.MWReferenceDialog.prototype.initialize = function () {
* @method
*/
ve.ui.MWReferenceDialog.prototype.onOpen = function () {
var focusedNode, data,
var focusedNode, data, refGroup, listKey,
doc = this.surface.getModel().getDocument();
// Parent method
@ -117,6 +117,8 @@ ve.ui.MWReferenceDialog.prototype.onOpen = function () {
if ( focusedNode instanceof ve.ce.MWReferenceNode ) {
this.internalItem = focusedNode.getModel().getInternalItem();
data = doc.getData( this.internalItem.getRange(), true );
listKey = focusedNode.getModel().getAttribute( 'listKey' );
refGroup = focusedNode.getModel().getAttribute( 'refGroup' );
} else {
data = [
{ 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
@ -124,13 +126,14 @@ ve.ui.MWReferenceDialog.prototype.onOpen = function () {
];
}
// Properties
this.referenceSurface = new ve.ui.Surface(
new ve.dm.ElementLinearData( doc.getStore(), data ), { '$$': this.frame.$$ }
);
this.referenceToolbar = new ve.ui.Toolbar( this.referenceSurface, { '$$': this.frame.$$ } );
// Initialization
this.nameInput.setValue( listKey );
this.groupInput.setValue( refGroup );
this.referenceToolbar.$.addClass( 've-ui-mwReferenceDialog-toolbar' );
this.contentFieldset.$.append( this.referenceToolbar.$, this.referenceSurface.$ );
this.referenceToolbar.addTools( this.constructor.static.toolbarTools );
@ -146,8 +149,9 @@ ve.ui.MWReferenceDialog.prototype.onOpen = function () {
* @param {string} action Action that caused the window to be closed
*/
ve.ui.MWReferenceDialog.prototype.onClose = function ( action ) {
var data, doc, groupName, key, newItem,
txs = [];
var data, doc, listIndex, listGroup, listKey, refGroup, newItem, refNode, oldListGroup,
oldListKey, oldNodes, internalList, attrChanges,
surfaceModel = this.surface.getModel();
// Parent method
ve.ui.Dialog.prototype.onOpen.call( this );
@ -155,32 +159,63 @@ ve.ui.MWReferenceDialog.prototype.onClose = function ( action ) {
// Save changes
if ( action === 'apply' ) {
data = this.referenceSurface.getModel().getDocument().getData();
doc = this.surface.getModel().getDocument();
doc = surfaceModel.getDocument();
listKey = this.nameInput.getValue() !== '' ? this.nameInput.getValue() : null;
refGroup = this.groupInput.getValue();
listGroup = 'mwReference/' + refGroup;
if ( this.internalItem ) {
txs.push(
ve.dm.Transaction.newFromNodeReplacement(
doc, this.internalItem.getRange(), data
)
);
this.surface.getModel().change( txs );
} else {
// TODO: pass in group and key from UI if they exist
groupName = '';
key = null;
newItem = doc.getInternalList().getItemInsertion( groupName, key, data );
if ( newItem.transaction ) {
txs.push( newItem.transaction );
// Edit reference: handle various replacement cases
refNode = this.surface.getView().getFocusedNode().getModel();
oldListGroup = refNode.getAttribute( 'listGroup' );
oldListKey = refNode.getAttribute( 'listKey' );
// Group/key has changed
if ( listGroup !== oldListGroup || listKey !== oldListKey ) {
internalList = doc.getInternalList();
oldNodes = internalList.getNodeGroup( oldListGroup ).keyedNodes[oldListKey] || [];
listIndex = internalList.getKeyIndex( listGroup, listKey );
attrChanges = {};
if ( listIndex !== undefined ) {
// If the new key exists, reuse its internal node
attrChanges.listIndex = listIndex;
} else if ( oldNodes.length > 1 ) {
// If the old internal node was being shared, create a new one
newItem = internalList.getItemInsertion( listGroup, listKey, data );
attrChanges.listIndex = newItem.index;
}
attrChanges.listGroup = listGroup;
attrChanges.listKey = listKey;
attrChanges.refGroup = refGroup;
// Manually re-register the node before and after change
refNode.removeFromInternalList();
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChanges(
doc, refNode.getOuterRange().start, attrChanges
)
);
refNode.addToInternalList();
}
this.surface.getModel().change( txs );
this.surface.getModel().getFragment().collapseRangeToEnd().insertContent( [
// Process the internal node create/edit transaction
if ( !newItem ) {
surfaceModel.change(
ve.dm.Transaction.newFromNodeReplacement( doc, this.internalItem, data )
);
} else {
surfaceModel.change( newItem.transaction );
}
} else {
// Create reference: just create new nodes
newItem = doc.getInternalList().getItemInsertion( listGroup, listKey, data );
surfaceModel.change( newItem.transaction );
surfaceModel.getFragment().collapseRangeToEnd().insertContent( [
{
'type': 'mwReference',
'attributes': {
'mw': { 'name': 'ref' },
'listIndex': newItem.index,
'listGroup': 'mwReference/' + groupName,
'listKey': key,
'refGroup': groupName
'listGroup': listGroup,
'listKey': listKey,
'refGroup': refGroup
}
},
{ 'type': '/mwReference' }