Node annotation blacklists

Allow nodes to specify a blacklist of annotations which can't be applied
to it.

Bug: 53151
Change-Id: Iac7649f4b38d8bfa94c64f734634afd45a5afc53
This commit is contained in:
Ed Sanders 2013-09-19 19:00:23 +01:00
parent ede7dad318
commit f001d306ad
5 changed files with 135 additions and 3 deletions

View file

@ -158,6 +158,15 @@ ve.dm.Node.static.parentNodeTypes = null;
*/
ve.dm.Node.static.suggestedParentNodeTypes = null;
/**
* Array of annotation types which can't be applied to this node
*
* @static
* @property {string[]} static.blacklistedAnnotationTypes
* @inheritable
*/
ve.dm.Node.static.blacklistedAnnotationTypes = [];
/**
* Default attributes to set for newly created linear model elements. These defaults will be used
* when creating a new element in ve.dm.NodeFactory#getDataElement when there is no DOM node or

View file

@ -154,6 +154,31 @@ ve.dm.NodeFactory.prototype.canNodeContainContent = function ( type ) {
throw new Error( 'Unknown node type: ' + type );
};
/**
* Check if node can take annotations of a specific type.
*
* @method
* @param {string} type Node type
* @param {ve.dm.Annotation} annotation Annotation to test
* @returns {boolean} Node can take annotations of this type
* @throws {Error} Unknown node type
*/
ve.dm.NodeFactory.prototype.canNodeTakeAnnotationType = function ( type, annotation ) {
if ( !( type in this.registry ) ) {
throw new Error( 'Unknown node type: ' + type );
}
var i, len,
blacklist = this.registry[type].static.blacklistedAnnotationTypes;
for ( i = 0, len = blacklist.length; i < len; i++ ) {
if ( annotation instanceof ve.dm.annotationFactory.create( blacklist[i] ).constructor ) {
return false;
}
}
return true;
};
/**
* Check if a node is content.
*

View file

@ -228,7 +228,7 @@ ve.dm.Transaction.newFromAttributeChanges = function ( doc, offset, attr ) {
* @returns {ve.dm.Transaction} Transaction that annotates content
*/
ve.dm.Transaction.newFromAnnotation = function ( doc, range, method, annotation ) {
var covered, type,
var covered, type, annotatable,
tx = new ve.dm.Transaction(),
data = doc.data,
i = range.start,
@ -237,9 +237,25 @@ ve.dm.Transaction.newFromAnnotation = function ( doc, range, method, annotation
insideContentNode = false;
// Iterate over all data in range, annotating where appropriate
while ( i < range.end ) {
if ( data.isElementData( i ) ) {
type = data.getType( i );
if ( ve.dm.nodeFactory.isNodeContent( type ) ) {
if ( method === 'set' && !ve.dm.nodeFactory.canNodeTakeAnnotationType( type, annotation ) ) {
// Blacklisted annotations can't be set
annotatable = false;
} else {
annotatable = true;
}
} else {
// Structural nodes are never annotatable
annotatable = false;
}
} else {
// Text is always annotatable
annotatable = true;
}
if (
( data.isElementData( i ) && !ve.dm.nodeFactory.isNodeContent( type ) ) ||
!annotatable ||
( insideContentNode && !data.isCloseElementData( i ) )
) {
// Structural element opening or closing, or entering a content node

View file

@ -764,6 +764,7 @@ QUnit.test( 'newFromAttributeChanges', function ( assert ) {
QUnit.test( 'newFromAnnotation', function ( assert ) {
var bold = ve.dm.example.createAnnotation( ve.dm.example.bold ),
doc = ve.dm.example.createExampleDocument(),
annotationDoc = ve.dm.example.createExampleDocument( 'annotationData' ),
cases = {
'over plain text': {
'args': [doc, new ve.Range( 1, 2 ), 'set', bold],
@ -858,8 +859,63 @@ QUnit.test( 'newFromAnnotation', function ( assert ) {
},
{ 'type': 'retain', 'length': 52 }
]
},
'over content and content element (image)': {
'args': [doc, new ve.Range( 38, 42 ), 'set', bold],
'ops': [
{ 'type': 'retain', 'length': 38 },
{
'type': 'annotate',
'method': 'set',
'bias': 'start',
'annotation': bold
},
{ 'type': 'retain', 'length': 4 },
{
'type': 'annotate',
'method': 'set',
'bias': 'stop',
'annotation': bold
},
{ 'type': 'retain', 'length': 21 }
]
},
'over content and unannotatable content element (unboldable node)': {
'args': [annotationDoc, new ve.Range( 1, 9 ), 'set', bold],
'ops': [
{ 'type': 'retain', 'length': 1 },
{
'type': 'annotate',
'method': 'set',
'bias': 'start',
'annotation': bold
},
{ 'type': 'retain', 'length': 3 },
{
'type': 'annotate',
'method': 'set',
'bias': 'stop',
'annotation': bold
},
{ 'type': 'retain', 'length': 2 },
{
'type': 'annotate',
'method': 'set',
'bias': 'start',
'annotation': bold
},
{ 'type': 'retain', 'length': 3 },
{
'type': 'annotate',
'method': 'set',
'bias': 'stop',
'annotation': bold
},
{ 'type': 'retain', 'length': 3 }
]
}
};
QUnit.expect( ve.getObjectKeys( cases ).length );
runConstructorTests( assert, ve.dm.Transaction.newFromAnnotation, cases );
} );

View file

@ -2839,6 +2839,32 @@ ve.dm.example.isolationData = [
// 246
];
ve.dm.example.UnboldableNode = function ( lenght, element ) {
// Parent constructor
ve.dm.LeafNode.call( this, 0, element );
};
ve.inheritClass( ve.dm.example.UnboldableNode, ve.dm.LeafNode );
ve.dm.example.UnboldableNode.static.name = 'exampleUnboldable';
ve.dm.example.UnboldableNode.static.isContent = true;
ve.dm.example.UnboldableNode.static.blacklistedAnnotationTypes = [ 'textStyle/bold' ];
ve.dm.example.UnboldableNode.static.matchTagNames = [];
ve.dm.modelRegistry.register( ve.dm.example.UnboldableNode );
ve.dm.example.annotationData = [
{ 'type': 'paragraph' },
'F',
'o',
'o',
{ 'type': 'exampleUnboldable' },
{ 'type': '/exampleUnboldable' },
'B',
'a',
'r',
{ 'type': '/paragraph' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
];
ve.dm.example.selectNodesCases = [
{
'range': new ve.Range( 1 ),