2012-07-19 00:11:26 +00:00
|
|
|
/**
|
|
|
|
* VisualEditor data model Transaction class.
|
2012-07-19 21:25:16 +00:00
|
|
|
*
|
2012-07-19 00:11:26 +00:00
|
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
2011-11-02 21:00:55 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* DataModel transaction.
|
|
|
|
*
|
2011-11-02 21:00:55 +00:00
|
|
|
* @class
|
|
|
|
* @constructor
|
|
|
|
*/
|
2012-09-06 23:15:55 +00:00
|
|
|
ve.dm.Transaction = function VeDmTransaction() {
|
2012-06-20 01:20:28 +00:00
|
|
|
this.operations = [];
|
2011-11-22 22:57:23 +00:00
|
|
|
this.lengthDifference = 0;
|
2011-11-02 21:00:55 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/* Static Methods */
|
2011-11-02 21:00:55 +00:00
|
|
|
|
2011-11-04 21:06:06 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* Generates a transaction that inserts data at a given offset.
|
|
|
|
*
|
|
|
|
* @static
|
2011-11-04 21:06:06 +00:00
|
|
|
* @method
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {ve.dm.Document} doc Document to create transaction for
|
Object management: Object create/inherit/clone utilities
* For the most common case:
- replace ve.extendClass with ve.inheritClass (chose slightly
different names to detect usage of the old/new one, and I
like 'inherit' better).
- move it up to below the constructor, see doc block for why.
* Cases where more than 2 arguments were passed to
ve.extendClass are handled differently depending on the case.
In case of a longer inheritance tree, the other arguments
could be omitted (like in "ve.ce.FooBar, ve.FooBar,
ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar,
because ve.ce.FooBar inherits from ve.Bar).
In the case of where it previously had two mixins with
ve.extendClass(), either one becomes inheritClass and one
a mixin, both to mixinClass().
No visible changes should come from this commit as the
instances still all have the same visible properties in the
end. No more or less than before.
* Misc.:
- Be consistent in calling parent constructors in the
same order as the inheritance.
- Add missing @extends and @param documentation.
- Replace invalid {Integer} type hint with {Number}.
- Consistent doc comments order:
@class, @abstract, @constructor, @extends, @params.
- Fix indentation errors
A fairly common mistake was a superfluous space before the
identifier on the assignment line directly below the
documentation comment.
$ ack "^ [^*]" --js modules/ve
- Typo "Inhertiance" -> "Inheritance".
- Replacing the other confusing comment "Inheritance" (inside
the constructor) with "Parent constructor".
- Add missing @abstract for ve.ui.Tool.
- Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js
- Add function names to all @constructor functions. Now that we
have inheritance it is important and useful to have these
functions not be anonymous.
Example of debug shot: http://cl.ly/image/1j3c160w3D45
Makes the difference between
< documentNode;
> ve_dm_DocumentNode
...
: ve_dm_BranchNode
...
: ve_dm_Node
...
: ve_dm_Node
...
: Object
...
without names (current situation):
< documentNode;
> Object
...
: Object
...
: Object
...
: Object
...
: Object
...
though before this commit, it really looks like this
(flattened since ve.extendClass really did a mixin):
< documentNode;
> Object
...
...
...
Pattern in Sublime (case-sensitive) to find nameless
constructor functions:
"^ve\..*\.([A-Z])([^\.]+) = function \("
Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
|
|
|
* @param {Number} offset Offset to insert at
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {Array} data Data to insert
|
|
|
|
* @returns {ve.dm.Transaction} Transcation that inserts data
|
2011-11-04 21:06:06 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.newFromInsertion = function ( doc, offset, insertion ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
var tx = new ve.dm.Transaction(),
|
|
|
|
data = doc.getData();
|
|
|
|
// Fix up the insertion
|
|
|
|
insertion = doc.fixupInsertion( insertion, offset );
|
|
|
|
// Retain up to insertion point, if needed
|
|
|
|
tx.pushRetain( offset );
|
|
|
|
// Insert data
|
|
|
|
tx.pushReplace( [], insertion );
|
|
|
|
// Retain to end of document, if needed (for completeness)
|
|
|
|
tx.pushRetain( data.length - offset );
|
|
|
|
return tx;
|
2011-11-02 21:00:55 +00:00
|
|
|
};
|
|
|
|
|
2011-11-17 22:42:18 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* Generates a transaction which removes data from a given range.
|
|
|
|
*
|
|
|
|
* There are three possible results from a removal:
|
|
|
|
* 1. Remove content only
|
|
|
|
* - Occurs when the range starts and ends on elements of different type, depth or ancestry
|
|
|
|
* 2. Remove entire elements and their content
|
|
|
|
* - Occurs when the range spans across an entire element
|
|
|
|
* 3. Merge two elements by removing the end of one and the beginning of another
|
|
|
|
* - Occurs when the range starts and ends on elements of similar type, depth and ancestry
|
|
|
|
*
|
|
|
|
* This function uses the following logic to decide what to actually remove:
|
|
|
|
* 1. Elements are only removed if range being removed covers the entire element
|
|
|
|
* 2. Elements can only be merged if ve.dm.Node.canBeMergedWith() returns true
|
|
|
|
* 3. Merges take place at the highest common ancestor
|
|
|
|
*
|
2011-11-17 22:42:18 +00:00
|
|
|
* @method
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {ve.dm.Document} doc Document to create transaction for
|
|
|
|
* @param {ve.Range} range Range of data to remove
|
|
|
|
* @returns {ve.dm.Transaction} Transcation that removes data
|
|
|
|
* @throws 'Invalid range, can not remove from {range.start} to {range.end}'
|
2011-11-17 22:42:18 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.newFromRemoval = function ( doc, range ) {
|
2012-08-02 18:46:13 +00:00
|
|
|
var i, selection, first, last, nodeStart, nodeEnd,
|
|
|
|
offset = 0,
|
|
|
|
removeStart = null,
|
|
|
|
removeEnd = null,
|
|
|
|
tx = new ve.dm.Transaction(),
|
|
|
|
data = doc.getData();
|
2012-06-20 01:20:28 +00:00
|
|
|
// Normalize and validate range
|
|
|
|
range.normalize();
|
|
|
|
if ( range.start === range.end ) {
|
|
|
|
// Empty range, nothing to remove, retain up to the end of the document (for completeness)
|
|
|
|
tx.pushRetain( data.length );
|
|
|
|
return tx;
|
|
|
|
}
|
|
|
|
// Select nodes and validate selection
|
2012-08-02 18:46:13 +00:00
|
|
|
selection = doc.selectNodes( range, 'covered' );
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( selection.length === 0 ) {
|
|
|
|
// Empty selection? Something is wrong!
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Invalid range, cannot remove from ' + range.start + ' to ' + range.end );
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
first = selection[0];
|
|
|
|
last = selection[selection.length - 1];
|
|
|
|
// If the first and last node are mergeable, merge them
|
|
|
|
if ( first.node.canBeMergedWith( last.node ) ) {
|
|
|
|
if ( !first.range && !last.range ) {
|
|
|
|
// First and last node are both completely covered, remove them
|
|
|
|
removeStart = first.nodeOuterRange.start;
|
|
|
|
removeEnd = last.nodeOuterRange.end;
|
|
|
|
} else {
|
|
|
|
// Either the first node or the last node is partially covered, so remove
|
|
|
|
// the selected content
|
|
|
|
removeStart = ( first.range || first.nodeRange ).start;
|
|
|
|
removeEnd = ( last.range || last.nodeRange ).end;
|
|
|
|
}
|
|
|
|
tx.pushRetain( removeStart );
|
|
|
|
tx.pushReplace( data.slice( removeStart, removeEnd ), [] );
|
|
|
|
tx.pushRetain( data.length - removeEnd );
|
|
|
|
// All done
|
|
|
|
return tx;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The selection wasn't mergeable, so remove nodes that are completely covered, and strip
|
|
|
|
// nodes that aren't
|
|
|
|
for ( i = 0; i < selection.length; i++ ) {
|
|
|
|
if ( !selection[i].range ) {
|
|
|
|
// Entire node is covered, remove it
|
|
|
|
nodeStart = selection[i].nodeOuterRange.start;
|
|
|
|
nodeEnd = selection[i].nodeOuterRange.end;
|
|
|
|
} else {
|
|
|
|
// Part of the node is covered, remove that range
|
|
|
|
nodeStart = selection[i].range.start;
|
|
|
|
nodeEnd = selection[i].range.end;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Merge contiguous removals. Only apply a removal when a gap appears, or at the
|
|
|
|
// end of the loop
|
|
|
|
if ( removeEnd === null ) {
|
|
|
|
// First removal
|
|
|
|
removeStart = nodeStart;
|
|
|
|
removeEnd = nodeEnd;
|
|
|
|
} else if ( removeEnd === nodeStart ) {
|
|
|
|
// Merge this removal into the previous one
|
|
|
|
removeEnd = nodeEnd;
|
|
|
|
} else {
|
|
|
|
// There is a gap between the previous removal and this one
|
|
|
|
|
|
|
|
// Push the previous removal first
|
|
|
|
tx.pushRetain( removeStart - offset );
|
|
|
|
tx.pushReplace( data.slice( removeStart, removeEnd ), [] );
|
|
|
|
offset = removeEnd;
|
|
|
|
|
|
|
|
// Now start this removal
|
|
|
|
removeStart = nodeStart;
|
|
|
|
removeEnd = nodeEnd;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Apply the last removal, if any
|
|
|
|
if ( removeEnd !== null ) {
|
|
|
|
tx.pushRetain( removeStart - offset );
|
|
|
|
tx.pushReplace( data.slice( removeStart, removeEnd ), [] );
|
|
|
|
offset = removeEnd;
|
|
|
|
}
|
|
|
|
// Retain up to the end of the document
|
|
|
|
tx.pushRetain( data.length - offset );
|
|
|
|
return tx;
|
2011-11-17 22:42:18 +00:00
|
|
|
};
|
|
|
|
|
2011-11-04 21:06:06 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* Generates a transaction that changes an attribute.
|
|
|
|
*
|
|
|
|
* @static
|
2011-11-04 21:06:06 +00:00
|
|
|
* @method
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {ve.dm.Document} doc Document to create transaction for
|
Object management: Object create/inherit/clone utilities
* For the most common case:
- replace ve.extendClass with ve.inheritClass (chose slightly
different names to detect usage of the old/new one, and I
like 'inherit' better).
- move it up to below the constructor, see doc block for why.
* Cases where more than 2 arguments were passed to
ve.extendClass are handled differently depending on the case.
In case of a longer inheritance tree, the other arguments
could be omitted (like in "ve.ce.FooBar, ve.FooBar,
ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar,
because ve.ce.FooBar inherits from ve.Bar).
In the case of where it previously had two mixins with
ve.extendClass(), either one becomes inheritClass and one
a mixin, both to mixinClass().
No visible changes should come from this commit as the
instances still all have the same visible properties in the
end. No more or less than before.
* Misc.:
- Be consistent in calling parent constructors in the
same order as the inheritance.
- Add missing @extends and @param documentation.
- Replace invalid {Integer} type hint with {Number}.
- Consistent doc comments order:
@class, @abstract, @constructor, @extends, @params.
- Fix indentation errors
A fairly common mistake was a superfluous space before the
identifier on the assignment line directly below the
documentation comment.
$ ack "^ [^*]" --js modules/ve
- Typo "Inhertiance" -> "Inheritance".
- Replacing the other confusing comment "Inheritance" (inside
the constructor) with "Parent constructor".
- Add missing @abstract for ve.ui.Tool.
- Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js
- Add function names to all @constructor functions. Now that we
have inheritance it is important and useful to have these
functions not be anonymous.
Example of debug shot: http://cl.ly/image/1j3c160w3D45
Makes the difference between
< documentNode;
> ve_dm_DocumentNode
...
: ve_dm_BranchNode
...
: ve_dm_Node
...
: ve_dm_Node
...
: Object
...
without names (current situation):
< documentNode;
> Object
...
: Object
...
: Object
...
: Object
...
: Object
...
though before this commit, it really looks like this
(flattened since ve.extendClass really did a mixin):
< documentNode;
> Object
...
...
...
Pattern in Sublime (case-sensitive) to find nameless
constructor functions:
"^ve\..*\.([A-Z])([^\.]+) = function \("
Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
|
|
|
* @param {Number} offset Offset of element
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {String} key Attribute name
|
2012-06-21 01:40:24 +00:00
|
|
|
* @param {Mixed} value New value, or undefined to remove the attribute
|
2012-06-20 01:20:28 +00:00
|
|
|
* @returns {ve.dm.Transaction} Transcation that changes an element
|
|
|
|
* @throws 'Can not set attributes to non-element data'
|
|
|
|
* @throws 'Can not set attributes on closing element'
|
2011-11-04 21:06:06 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.newFromAttributeChange = function ( doc, offset, key, value ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
var tx = new ve.dm.Transaction(),
|
|
|
|
data = doc.getData();
|
|
|
|
// Verify element exists at offset
|
|
|
|
if ( data[offset].type === undefined ) {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Can not set attributes to non-element data' );
|
2011-11-23 23:54:36 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
// Verify element is not a closing
|
|
|
|
if ( data[offset].type.charAt( 0 ) === '/' ) {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Can not set attributes on closing element' );
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
// Retain up to element
|
|
|
|
tx.pushRetain( offset );
|
|
|
|
// Change attribute
|
|
|
|
tx.pushReplaceElementAttribute(
|
|
|
|
key, 'attributes' in data[offset] ? data[offset].attributes[key] : undefined, value
|
|
|
|
);
|
|
|
|
// Retain to end of document
|
|
|
|
tx.pushRetain( data.length - offset );
|
|
|
|
return tx;
|
2011-11-02 21:00:55 +00:00
|
|
|
};
|
|
|
|
|
2011-11-04 21:06:06 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* Generates a transaction that annotates content.
|
|
|
|
*
|
|
|
|
* @static
|
2011-11-04 21:06:06 +00:00
|
|
|
* @method
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {ve.dm.Document} doc Document to create transaction for
|
|
|
|
* @param {ve.Range} range Range to annotate
|
|
|
|
* @param {String} method Annotation mode
|
|
|
|
* 'set': Adds annotation to all content in range
|
|
|
|
* 'clear': Removes instances of annotation from content in range
|
|
|
|
* @param {Object} annotation Annotation to set or clear
|
2012-08-24 02:06:36 +00:00
|
|
|
* @returns {ve.dm.Transaction} Transaction that annotates content
|
2011-11-04 21:06:06 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.newFromAnnotation = function ( doc, range, method, annotation ) {
|
2012-08-02 18:46:13 +00:00
|
|
|
var covered,
|
|
|
|
tx = new ve.dm.Transaction(),
|
|
|
|
data = doc.getData(),
|
|
|
|
i = range.start,
|
2012-06-20 01:20:28 +00:00
|
|
|
span = i,
|
|
|
|
on = false;
|
2012-08-02 18:46:13 +00:00
|
|
|
// Iterate over all data in range, annotating where appropriate
|
|
|
|
range.normalize();
|
2012-06-20 01:20:28 +00:00
|
|
|
while ( i < range.end ) {
|
|
|
|
if ( data[i].type !== undefined ) {
|
|
|
|
// Element
|
|
|
|
if ( on ) {
|
|
|
|
tx.pushRetain( span );
|
|
|
|
tx.pushStopAnnotating( method, annotation );
|
|
|
|
span = 0;
|
|
|
|
on = false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Content
|
2012-08-02 18:46:13 +00:00
|
|
|
covered = doc.offsetContainsAnnotation( i, annotation );
|
Remainder JSHint fixes on modules/ve/*
[jshint]
ce/ve.ce.Surface.js: line 670, col 9, Too many var statements.
ce/ve.ce.Surface.js: line 695, col 6, Missing semicolon.
ce/ve.ce.Surface.js: line 726, col 22, Expected '===' and instead saw '=='.
ce/ve.ce.Surface.js: line 726, col 41, Expected '===' and instead saw '=='.
ce/ve.ce.Surface.js: line 733, col 13, Too many var statements.
ce/ve.ce.Surface.js: line 734, col 24, Expected '===' and instead saw '=='.
ce/ve.ce.Surface.js: line 1013, col 13, Too many var statements.
ce/ve.ce.Surface.js: line 1019, col 17, Too many var statements.
ce/ve.ce.Surface.js: line 1023, col 18, Too many ar statements.
ce/ve.ce.Surface.js: line 1027, col 13, Too many var statements.
dm/annotations/ve.dm.LinkAnnotation.js: line 70, col 52, Insecure '.'.
dm/ve.dm.Converter.js: line 383, col 29, Empty block.
dm/ve.dm.Converter.js: line 423, col 33, Empty block.
Commands:
* jshint .
* ack '(if|else|function|switch|for|while)\('
* Sublime Text 2:
Find(*): (if|else|function|switch|for|while)\(
Replace: $1 (
* ack ' ' -Q # double spaces, except in certain comments
Change-Id: I8e34bf2924bc8688fdf8acef08bbc4f6707e93be
2012-09-02 21:45:01 +00:00
|
|
|
if ( ( covered && method === 'set' ) || ( !covered && method === 'clear' ) ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
// Skip annotated content
|
|
|
|
if ( on ) {
|
|
|
|
tx.pushRetain( span );
|
|
|
|
tx.pushStopAnnotating( method, annotation );
|
|
|
|
span = 0;
|
|
|
|
on = false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Cover non-annotated content
|
|
|
|
if ( !on ) {
|
|
|
|
tx.pushRetain( span );
|
|
|
|
tx.pushStartAnnotating( method, annotation );
|
|
|
|
span = 0;
|
|
|
|
on = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
span++;
|
|
|
|
i++;
|
2011-11-23 23:54:36 +00:00
|
|
|
}
|
2012-06-20 01:20:28 +00:00
|
|
|
tx.pushRetain( span );
|
|
|
|
if ( on ) {
|
|
|
|
tx.pushStopAnnotating( method, annotation );
|
|
|
|
}
|
|
|
|
tx.pushRetain( data.length - range.end );
|
|
|
|
return tx;
|
2011-11-02 21:00:55 +00:00
|
|
|
};
|
|
|
|
|
2011-11-04 21:06:06 +00:00
|
|
|
/**
|
2012-06-20 01:20:28 +00:00
|
|
|
* Generates a transaction that converts elements that can contain content.
|
|
|
|
*
|
|
|
|
* @static
|
2011-11-04 21:06:06 +00:00
|
|
|
* @method
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {ve.dm.Document} doc Document to create transaction for
|
|
|
|
* @param {ve.Range} range Range to convert
|
|
|
|
* @param {String} type Symbolic name of element type to convert to
|
|
|
|
* @param {Object} attr Attributes to initialize element with
|
2012-08-24 02:06:36 +00:00
|
|
|
* @returns {ve.dm.Transaction} Transaction that converts content branches
|
2011-11-04 21:06:06 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.newFromContentBranchConversion = function ( doc, range, type, attr ) {
|
2012-08-02 18:46:13 +00:00
|
|
|
var i, selected, branch, branchOuterRange,
|
|
|
|
tx = new ve.dm.Transaction(),
|
2012-06-20 01:20:28 +00:00
|
|
|
data = doc.getData(),
|
|
|
|
selection = doc.selectNodes( range, 'leaves' ),
|
|
|
|
opening = { 'type': type },
|
|
|
|
closing = { 'type': '/' + type },
|
|
|
|
previousBranch,
|
|
|
|
previousBranchOuterRange;
|
|
|
|
// Add attributes to opening if needed
|
|
|
|
if ( ve.isPlainObject( attr ) ) {
|
|
|
|
opening.attributes = attr;
|
|
|
|
}
|
|
|
|
// Replace the wrappings of each content branch in the range
|
2012-08-02 18:46:13 +00:00
|
|
|
for ( i = 0; i < selection.length; i++ ) {
|
|
|
|
selected = selection[i];
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( selected.node.isContent() ) {
|
2012-08-02 18:46:13 +00:00
|
|
|
branch = selected.node.getParent();
|
2012-08-10 22:35:48 +00:00
|
|
|
// Skip branches that are already of the target type and have identical attributes
|
|
|
|
if ( branch.getType() === type && ve.compareObjects( branch.getAttributes(), attr ) ) {
|
|
|
|
continue;
|
|
|
|
}
|
2012-08-02 18:46:13 +00:00
|
|
|
branchOuterRange = branch.getOuterRange();
|
2012-06-20 01:20:28 +00:00
|
|
|
// Don't convert the same branch twice
|
|
|
|
if ( branch === previousBranch ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Retain up to this branch, considering where the previous one left off
|
|
|
|
tx.pushRetain(
|
|
|
|
branchOuterRange.start - ( previousBranch ? previousBranchOuterRange.end : 0 )
|
|
|
|
);
|
|
|
|
// Replace the opening
|
|
|
|
tx.pushReplace( [data[branchOuterRange.start]], [ve.copyObject( opening )] );
|
|
|
|
// Retain the contents
|
|
|
|
tx.pushRetain( branch.getLength() );
|
|
|
|
// Replace the closing
|
|
|
|
tx.pushReplace( [data[branchOuterRange.end - 1]], [ve.copyObject( closing )] );
|
|
|
|
// Remember this branch and its range for next time
|
|
|
|
previousBranch = branch;
|
|
|
|
previousBranchOuterRange = branchOuterRange;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Retain until the end
|
|
|
|
tx.pushRetain(
|
|
|
|
data.length - ( previousBranch ? previousBranchOuterRange.end : 0 )
|
|
|
|
);
|
|
|
|
return tx;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a transaction which wraps, unwraps or replaces structure.
|
|
|
|
*
|
|
|
|
* The unwrap parameters are checked against the actual model data, and
|
|
|
|
* an exception is thrown if the type fields don't match. This means you
|
|
|
|
* can omit attributes from the unwrap parameters, those are automatically
|
|
|
|
* picked up from the model data instead.
|
|
|
|
*
|
|
|
|
* NOTE: This function currently does not fix invalid parent/child relationships, so it will
|
|
|
|
* happily convert paragraphs to listItems without wrapping them in a list if that's what you
|
|
|
|
* ask it to do. We'll probably fix this later but for now the caller is responsible for giving
|
|
|
|
* valid instructions.
|
|
|
|
*
|
|
|
|
* @param {ve.dm.Document} doc Document to generate a transaction for
|
|
|
|
* @param {ve.Range} range Range to wrap/unwrap/replace around
|
|
|
|
* @param {Array} unwrapOuter Array of opening elements to unwrap. These must be immediately *outside* the range.
|
|
|
|
* @param {Array} wrapOuter Array of opening elements to wrap around the range.
|
|
|
|
* @param {Array} unwrapEach Array of opening elements to unwrap from each top-level element in the range.
|
|
|
|
* @param {Array} wrapEach Array of opening elements to wrap around each top-level element in the range.
|
|
|
|
* @returns {ve.dm.Transaction}
|
|
|
|
*
|
|
|
|
* @example Changing a paragraph to a header:
|
|
|
|
* Before: [ {'type': 'paragraph'}, 'a', 'b', 'c', {'type': '/paragraph'} ]
|
|
|
|
* newFromWrap( new ve.Range( 1, 4 ), [ {'type': 'paragraph'} ], [ {'type': 'heading', 'level': 1 } ] );
|
|
|
|
* After: [ {'type': 'heading', 'level': 1 }, 'a', 'b', 'c', {'type': '/heading'} ]
|
|
|
|
*
|
|
|
|
* @example Changing a set of paragraphs to a list:
|
|
|
|
* Before: [ {'type': 'paragraph'}, 'a', {'type': '/paragraph'}, {'type':'paragraph'}, 'b', {'type':'/paragraph'} ]
|
|
|
|
* newFromWrap( new ve.Range( 0, 6 ), [], [ {'type': 'list' } ], [], [ {'type': 'listItem', 'attributes': {'styles': ['bullet']}} ] );
|
|
|
|
* After: [ {'type': 'list'}, {'type': 'listItem', 'attributes': {'styles': ['bullet']}}, {'type':'paragraph'} 'a',
|
|
|
|
* {'type': '/paragraph'}, {'type': '/listItem'}, {'type': 'listItem', 'attributes': {'styles': ['bullet']}},
|
|
|
|
* {'type': 'paragraph'}, 'b', {'type': '/paragraph'}, {'type': '/listItem'}, {'type': '/list'} ]
|
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.newFromWrap = function ( doc, range, unwrapOuter, wrapOuter, unwrapEach, wrapEach ) {
|
2012-08-02 18:46:13 +00:00
|
|
|
var i, j, unwrapOuterData, startOffset, unwrapEachData, closingUnwrapEach, closingWrapEach,
|
|
|
|
tx = new ve.dm.Transaction(),
|
|
|
|
depth = 0;
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
// Function to generate arrays of closing elements in reverse order
|
|
|
|
function closingArray( openings ) {
|
|
|
|
var closings = [], i, len = openings.length;
|
|
|
|
for ( i = 0; i < len; i++ ) {
|
|
|
|
closings[closings.length] = { 'type': '/' + openings[len - i - 1].type };
|
|
|
|
}
|
|
|
|
return closings;
|
|
|
|
}
|
2012-08-02 18:46:13 +00:00
|
|
|
closingUnwrapEach = closingArray( unwrapEach );
|
|
|
|
closingWrapEach = closingArray( wrapEach );
|
2012-06-20 01:20:28 +00:00
|
|
|
|
|
|
|
// TODO: check for and fix nesting validity like fixupInsertion does
|
|
|
|
range.normalize();
|
|
|
|
if ( range.start > unwrapOuter.length ) {
|
|
|
|
// Retain up to the first thing we're unwrapping
|
|
|
|
// The outer unwrapping takes place *outside*
|
|
|
|
// the range, so compensate for that
|
|
|
|
tx.pushRetain( range.start - unwrapOuter.length );
|
|
|
|
} else if ( range.start < unwrapOuter.length ) {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'unwrapOuter is longer than the data preceding the range' );
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Replace the opening elements for the outer unwrap&wrap
|
|
|
|
if ( wrapOuter.length > 0 || unwrapOuter.length > 0 ) {
|
|
|
|
// Verify that wrapOuter matches the data at this position
|
|
|
|
unwrapOuterData = doc.data.slice( range.start - unwrapOuter.length, range.start );
|
|
|
|
for ( i = 0; i < unwrapOuterData.length; i++ ) {
|
|
|
|
if ( unwrapOuterData[i].type !== unwrapOuter[i].type ) {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Element in unwrapOuter does not match: expected ' +
|
2012-08-06 20:38:00 +00:00
|
|
|
unwrapOuter[i].type + ' but found ' + unwrapOuterData[i].type );
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Instead of putting in unwrapOuter as given, put it in the
|
|
|
|
// way it appears in the mode,l so we pick up any attributes
|
|
|
|
tx.pushReplace( unwrapOuterData, ve.copyArray( wrapOuter ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( wrapEach.length > 0 || unwrapEach.length > 0 ) {
|
|
|
|
// Visit each top-level child and wrap/unwrap it
|
|
|
|
// TODO figure out if we should use the tree/node functions here
|
|
|
|
// rather than iterating over offsets, it may or may not be faster
|
|
|
|
for ( i = range.start; i < range.end; i++ ) {
|
2012-07-19 03:40:49 +00:00
|
|
|
if ( doc.data[i].type !== undefined ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
// This is a structural offset
|
2012-07-19 03:40:49 +00:00
|
|
|
if ( doc.data[i].type.charAt( 0 ) !== '/' ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
// This is an opening element
|
|
|
|
if ( depth === 0 ) {
|
|
|
|
// We are at the start of a top-level element
|
|
|
|
// Replace the opening elements
|
|
|
|
|
|
|
|
// Verify that unwrapEach matches the data at this position
|
|
|
|
unwrapEachData = doc.data.slice( i, i + unwrapEach.length );
|
|
|
|
for ( j = 0; j < unwrapEachData.length; j++ ) {
|
|
|
|
if ( unwrapEachData[j].type !== unwrapEach[j].type ) {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Element in unwrapEach does not match: expected ' +
|
2012-06-20 01:20:28 +00:00
|
|
|
unwrapEach[j].type + ' but found ' +
|
2012-08-06 20:38:00 +00:00
|
|
|
unwrapEachData[j].type );
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Instead of putting in unwrapEach as given, put it in the
|
|
|
|
// way it appears in the model, so we pick up any attributes
|
|
|
|
tx.pushReplace( ve.copyArray( unwrapEachData ), ve.copyArray( wrapEach ) );
|
|
|
|
|
|
|
|
// Store this offset for later
|
|
|
|
startOffset = i;
|
|
|
|
}
|
|
|
|
depth++;
|
|
|
|
} else {
|
|
|
|
// This is a closing element
|
|
|
|
depth--;
|
|
|
|
if ( depth === 0 ) {
|
|
|
|
// We are at the end of a top-level element
|
|
|
|
// Retain the contents of what we're wrapping
|
|
|
|
tx.pushRetain( i - startOffset + 1 - unwrapEach.length*2 );
|
|
|
|
// Replace the closing elements
|
|
|
|
tx.pushReplace( ve.copyArray( closingUnwrapEach ), ve.copyArray( closingWrapEach ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-11-23 23:54:36 +00:00
|
|
|
} else {
|
2012-06-20 01:20:28 +00:00
|
|
|
// There is no wrapEach/unwrapEach to be done, just retain
|
|
|
|
// up to the end of the range
|
|
|
|
tx.pushRetain( range.end - range.start );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( wrapOuter.length > 0 || unwrapOuter.length > 0 ) {
|
|
|
|
tx.pushReplace( closingArray( unwrapOuter ), closingArray( wrapOuter ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retain up to the end of the document
|
|
|
|
if ( range.end < doc.data.length ) {
|
|
|
|
tx.pushRetain( doc.data.length - range.end - unwrapOuter.length );
|
|
|
|
}
|
|
|
|
|
|
|
|
return tx;
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Methods */
|
|
|
|
|
2012-08-10 22:35:48 +00:00
|
|
|
/**
|
|
|
|
* Checks if transaction would make any actual changes if processed.
|
|
|
|
*
|
|
|
|
* There may be more sophisticated checks that can be done, like looking for things being replaced
|
|
|
|
* with identical content, but such transactions probably should not be created in the first place.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {Boolean} Transaction is no-op
|
|
|
|
*/
|
|
|
|
ve.dm.Transaction.prototype.isNoOp = function () {
|
|
|
|
return (
|
|
|
|
this.operations.length === 0 ||
|
|
|
|
( this.operations.length === 1 && this.operations[0].type === 'retain' )
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* Gets a list of all operations.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {Object[]} List of operations
|
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.getOperations = function () {
|
2012-06-20 01:20:28 +00:00
|
|
|
return this.operations;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the difference in content length this transaction will cause if applied.
|
|
|
|
*
|
|
|
|
* @method
|
Object management: Object create/inherit/clone utilities
* For the most common case:
- replace ve.extendClass with ve.inheritClass (chose slightly
different names to detect usage of the old/new one, and I
like 'inherit' better).
- move it up to below the constructor, see doc block for why.
* Cases where more than 2 arguments were passed to
ve.extendClass are handled differently depending on the case.
In case of a longer inheritance tree, the other arguments
could be omitted (like in "ve.ce.FooBar, ve.FooBar,
ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar,
because ve.ce.FooBar inherits from ve.Bar).
In the case of where it previously had two mixins with
ve.extendClass(), either one becomes inheritClass and one
a mixin, both to mixinClass().
No visible changes should come from this commit as the
instances still all have the same visible properties in the
end. No more or less than before.
* Misc.:
- Be consistent in calling parent constructors in the
same order as the inheritance.
- Add missing @extends and @param documentation.
- Replace invalid {Integer} type hint with {Number}.
- Consistent doc comments order:
@class, @abstract, @constructor, @extends, @params.
- Fix indentation errors
A fairly common mistake was a superfluous space before the
identifier on the assignment line directly below the
documentation comment.
$ ack "^ [^*]" --js modules/ve
- Typo "Inhertiance" -> "Inheritance".
- Replacing the other confusing comment "Inheritance" (inside
the constructor) with "Parent constructor".
- Add missing @abstract for ve.ui.Tool.
- Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js
- Add function names to all @constructor functions. Now that we
have inheritance it is important and useful to have these
functions not be anonymous.
Example of debug shot: http://cl.ly/image/1j3c160w3D45
Makes the difference between
< documentNode;
> ve_dm_DocumentNode
...
: ve_dm_BranchNode
...
: ve_dm_Node
...
: ve_dm_Node
...
: Object
...
without names (current situation):
< documentNode;
> Object
...
: Object
...
: Object
...
: Object
...
: Object
...
though before this commit, it really looks like this
(flattened since ve.extendClass really did a mixin):
< documentNode;
> Object
...
...
...
Pattern in Sublime (case-sensitive) to find nameless
constructor functions:
"^ve\..*\.([A-Z])([^\.]+) = function \("
Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
|
|
|
* @returns {Number} Difference in content length
|
2012-06-20 01:20:28 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.getLengthDifference = function () {
|
2012-06-20 01:20:28 +00:00
|
|
|
return this.lengthDifference;
|
|
|
|
};
|
|
|
|
|
2012-06-20 21:37:13 +00:00
|
|
|
/**
|
2012-06-21 07:08:23 +00:00
|
|
|
* Translate an offset based on a transaction.
|
2012-06-20 21:37:13 +00:00
|
|
|
*
|
2012-06-21 07:08:23 +00:00
|
|
|
* This is useful when you want to anticipate what an offset will be after a transaction is
|
|
|
|
* processed.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {Number} offset Offset in the linear model before the transaction has been processed
|
|
|
|
* @returns {Number} Translated offset, as it will be after processing transaction
|
2012-06-20 21:37:13 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.translateOffset = function ( offset ) {
|
2012-06-28 11:24:08 +00:00
|
|
|
var i, cursor = 0, adjustment = 0, op;
|
2012-06-20 21:37:13 +00:00
|
|
|
for ( i = 0; i < this.operations.length; i++ ) {
|
|
|
|
op = this.operations[i];
|
|
|
|
if ( op.type === 'replace' ) {
|
|
|
|
adjustment += op.insert.length - op.remove.length;
|
|
|
|
if ( offset === cursor + op.remove.length ) {
|
|
|
|
// Offset points to right after the removal, translate it
|
|
|
|
return offset + adjustment;
|
2012-08-30 22:26:43 +00:00
|
|
|
} else if ( offset >= cursor && offset < cursor + op.remove.length ) {
|
2012-06-20 21:37:13 +00:00
|
|
|
// The offset points inside of the removal
|
2012-06-21 07:08:23 +00:00
|
|
|
return cursor + op.remove.length + adjustment;
|
2012-06-20 21:37:13 +00:00
|
|
|
}
|
|
|
|
cursor += op.remove.length;
|
|
|
|
} else if ( op.type === 'retain' ) {
|
2012-08-30 22:26:43 +00:00
|
|
|
if ( offset >= cursor && offset < cursor + op.length ) {
|
2012-06-20 21:37:13 +00:00
|
|
|
return offset + adjustment;
|
|
|
|
}
|
|
|
|
cursor += op.length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return offset + adjustment;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2012-06-21 07:08:23 +00:00
|
|
|
* Translate a range based on a transaction.
|
|
|
|
*
|
|
|
|
* This is useful when you want to anticipate what a selection will be after a transaction is
|
|
|
|
* processed.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @see {translateOffset}
|
|
|
|
* @param {ve.Range} range Range in the linear model before the transaction has been processed
|
|
|
|
* @returns {ve.Range} Translated range, as it will be after processing transaction
|
2012-06-20 21:37:13 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.translateRange = function ( range ) {
|
2012-06-21 07:08:23 +00:00
|
|
|
return new ve.Range( this.translateOffset( range.from ), this.translateOffset( range.to ) );
|
2012-06-20 21:37:13 +00:00
|
|
|
};
|
|
|
|
|
2012-06-20 01:20:28 +00:00
|
|
|
/**
|
|
|
|
* Adds a retain operation.
|
|
|
|
*
|
|
|
|
* @method
|
Object management: Object create/inherit/clone utilities
* For the most common case:
- replace ve.extendClass with ve.inheritClass (chose slightly
different names to detect usage of the old/new one, and I
like 'inherit' better).
- move it up to below the constructor, see doc block for why.
* Cases where more than 2 arguments were passed to
ve.extendClass are handled differently depending on the case.
In case of a longer inheritance tree, the other arguments
could be omitted (like in "ve.ce.FooBar, ve.FooBar,
ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar,
because ve.ce.FooBar inherits from ve.Bar).
In the case of where it previously had two mixins with
ve.extendClass(), either one becomes inheritClass and one
a mixin, both to mixinClass().
No visible changes should come from this commit as the
instances still all have the same visible properties in the
end. No more or less than before.
* Misc.:
- Be consistent in calling parent constructors in the
same order as the inheritance.
- Add missing @extends and @param documentation.
- Replace invalid {Integer} type hint with {Number}.
- Consistent doc comments order:
@class, @abstract, @constructor, @extends, @params.
- Fix indentation errors
A fairly common mistake was a superfluous space before the
identifier on the assignment line directly below the
documentation comment.
$ ack "^ [^*]" --js modules/ve
- Typo "Inhertiance" -> "Inheritance".
- Replacing the other confusing comment "Inheritance" (inside
the constructor) with "Parent constructor".
- Add missing @abstract for ve.ui.Tool.
- Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js
- Add function names to all @constructor functions. Now that we
have inheritance it is important and useful to have these
functions not be anonymous.
Example of debug shot: http://cl.ly/image/1j3c160w3D45
Makes the difference between
< documentNode;
> ve_dm_DocumentNode
...
: ve_dm_BranchNode
...
: ve_dm_Node
...
: ve_dm_Node
...
: Object
...
without names (current situation):
< documentNode;
> Object
...
: Object
...
: Object
...
: Object
...
: Object
...
though before this commit, it really looks like this
(flattened since ve.extendClass really did a mixin):
< documentNode;
> Object
...
...
...
Pattern in Sublime (case-sensitive) to find nameless
constructor functions:
"^ve\..*\.([A-Z])([^\.]+) = function \("
Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
|
|
|
* @param {Number} length Length of content data to retain
|
2012-06-20 01:20:28 +00:00
|
|
|
* @throws 'Invalid retain length, can not retain backwards: {length}'
|
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.pushRetain = function ( length ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( length < 0 ) {
|
2012-08-08 17:48:53 +00:00
|
|
|
throw new Error( 'Invalid retain length, can not retain backwards:' + length );
|
2012-06-20 01:20:28 +00:00
|
|
|
}
|
|
|
|
if ( length ) {
|
|
|
|
var end = this.operations.length - 1;
|
|
|
|
if ( this.operations.length && this.operations[end].type === 'retain' ) {
|
|
|
|
this.operations[end].length += length;
|
|
|
|
} else {
|
|
|
|
this.operations.push( {
|
|
|
|
'type': 'retain',
|
|
|
|
'length': length
|
|
|
|
} );
|
|
|
|
}
|
2011-11-23 23:54:36 +00:00
|
|
|
}
|
2011-11-02 21:00:55 +00:00
|
|
|
};
|
|
|
|
|
2012-03-08 23:21:17 +00:00
|
|
|
/**
|
|
|
|
* Adds a replace operation
|
2012-06-20 01:20:28 +00:00
|
|
|
*
|
2012-03-08 23:21:17 +00:00
|
|
|
* @method
|
|
|
|
* @param {Array} remove Data to remove
|
2012-06-20 01:20:28 +00:00
|
|
|
* @param {Array] insert Data to replace 'remove' with
|
2012-03-08 23:21:17 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.pushReplace = function ( remove, insert ) {
|
2012-06-20 01:20:28 +00:00
|
|
|
if ( remove.length === 0 && insert.length === 0 ) {
|
|
|
|
// Don't push no-ops
|
|
|
|
return;
|
|
|
|
}
|
2012-03-08 23:21:17 +00:00
|
|
|
this.operations.push( {
|
|
|
|
'type': 'replace',
|
|
|
|
'remove': remove,
|
2012-06-20 01:20:28 +00:00
|
|
|
'insert': insert
|
2012-03-08 23:21:17 +00:00
|
|
|
} );
|
2012-06-20 01:20:28 +00:00
|
|
|
this.lengthDifference += insert.length - remove.length;
|
2012-03-08 23:21:17 +00:00
|
|
|
};
|
|
|
|
|
2011-11-04 21:06:06 +00:00
|
|
|
/**
|
|
|
|
* Adds an element attribute change operation.
|
2012-06-20 01:20:28 +00:00
|
|
|
*
|
2011-11-04 21:06:06 +00:00
|
|
|
* @method
|
|
|
|
* @param {String} key Name of attribute to change
|
2012-06-21 01:40:24 +00:00
|
|
|
* @param {Mixed} from Value change attribute from, or undefined if not previously set
|
|
|
|
* @param {Mixed} to Value to change attribute to, or undefined to remove
|
2011-11-04 21:06:06 +00:00
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.pushReplaceElementAttribute = function ( key, from, to ) {
|
2011-11-02 21:00:55 +00:00
|
|
|
this.operations.push( {
|
|
|
|
'type': 'attribute',
|
|
|
|
'key': key,
|
2012-02-22 21:23:28 +00:00
|
|
|
'from': from,
|
|
|
|
'to': to
|
2011-11-02 21:00:55 +00:00
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
2011-11-04 21:06:06 +00:00
|
|
|
/**
|
|
|
|
* Adds a start annotating operation.
|
2012-06-20 01:20:28 +00:00
|
|
|
*
|
2011-11-04 21:06:06 +00:00
|
|
|
* @method
|
|
|
|
* @param {String} method Method to use, either "set" or "clear"
|
|
|
|
* @param {Object} annotation Annotation object to start setting or clearing from content data
|
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.pushStartAnnotating = function ( method, annotation ) {
|
2011-11-02 21:00:55 +00:00
|
|
|
this.operations.push( {
|
|
|
|
'type': 'annotate',
|
|
|
|
'method': method,
|
|
|
|
'bias': 'start',
|
|
|
|
'annotation': annotation
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
2011-11-04 21:06:06 +00:00
|
|
|
/**
|
|
|
|
* Adds a stop annotating operation.
|
2012-06-20 01:20:28 +00:00
|
|
|
*
|
2011-11-04 21:06:06 +00:00
|
|
|
* @method
|
|
|
|
* @param {String} method Method to use, either "set" or "clear"
|
|
|
|
* @param {Object} annotation Annotation object to stop setting or clearing from content data
|
|
|
|
*/
|
2012-08-07 01:50:44 +00:00
|
|
|
ve.dm.Transaction.prototype.pushStopAnnotating = function ( method, annotation ) {
|
2011-11-02 21:00:55 +00:00
|
|
|
this.operations.push( {
|
|
|
|
'type': 'annotate',
|
|
|
|
'method': method,
|
|
|
|
'bias': 'stop',
|
|
|
|
'annotation': annotation
|
|
|
|
} );
|
|
|
|
};
|