Index: trunk/extensions/VisualEditor/tests/es/index.html |
— | — | @@ -19,6 +19,7 @@ |
20 | 20 | <script src="../../modules/es/es.js"></script> |
21 | 21 | <script src="../../modules/es/es.Range.js"></script> |
22 | 22 | <script src="../../modules/es/es.Transaction.js"></script> |
| 23 | + <script src="../../modules/es/es.TransactionProcessor.js"></script> |
23 | 24 | |
24 | 25 | <!-- Bases --> |
25 | 26 | <script src="../../modules/es/bases/es.EventEmitter.js"></script> |
Index: trunk/extensions/VisualEditor/demo/index.html |
— | — | @@ -59,6 +59,7 @@ |
60 | 60 | <script src="../modules/es/es.Position.js"></script> |
61 | 61 | <script src="../modules/es/es.Range.js"></script> |
62 | 62 | <script src="../modules/es/es.Transaction.js"></script> |
| 63 | + <script src="../modules/es/es.TransactionProcessor.js"></script> |
63 | 64 | |
64 | 65 | <!-- Serializers --> |
65 | 66 | <script src="../modules/es/serializers/es.AnnotationSerializer.js"></script> |
Index: trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js |
— | — | @@ -59,271 +59,6 @@ |
60 | 60 | */ |
61 | 61 | es.DocumentModel.nodeRules = {}; |
62 | 62 | |
63 | | -/** |
64 | | - * Mapping of operation types to pure functions. |
65 | | - * |
66 | | - * Each function is called in the context of a state, and takes an operation object as a parameter. |
67 | | - */ |
68 | | -es.DocumentModel.operations = ( function() { |
69 | | - |
70 | | - // Pure functions |
71 | | - |
72 | | - function rebuild( newData, oldNodes ) { |
73 | | - var parent = oldNodes[0].getParent(), |
74 | | - index = parent.indexOf( oldNodes[0] ); |
75 | | - // Remove the node we are about to insert into from the model tree |
76 | | - parent.splice( index, oldNodes.length ); |
77 | | - // Regenerate nodes for the data we've affected |
78 | | - var newNodes = es.DocumentModel.createNodesFromData( newData ); |
79 | | - // Insert new elements into the tree where the old ones used to be |
80 | | - parent.splice.apply( parent, [index, 0].concat( newNodes ) ); |
81 | | - } |
82 | | - |
83 | | - function scope( node, data ) { |
84 | | - var i, |
85 | | - length, |
86 | | - level = 0, |
87 | | - maxDepth = 0; |
88 | | - for ( i = 0, length = data.length; i < length; i++ ) { |
89 | | - if ( typeof data[i].type === 'string' ) { |
90 | | - level += data[i].type.charAt( 0 ) === '/' ? -1 : 1; |
91 | | - maxDepth = Math.max( maxDepth, -level ); |
92 | | - } |
93 | | - } |
94 | | - if ( maxDepth > 0 ) { |
95 | | - for ( i = 0; i < maxDepth; i++ ) { |
96 | | - node = node.getParent(); |
97 | | - } |
98 | | - } |
99 | | - return node; |
100 | | - } |
101 | | - |
102 | | - // Methods (call in context of state) |
103 | | - |
104 | | - function retain( op ) { |
105 | | - annotate.call( this, this.cursor + op.length ); |
106 | | - this.cursor += op.length; |
107 | | - } |
108 | | - |
109 | | - function insert( op ) { |
110 | | - if ( es.DocumentModel.isStructuralOffset( this.data, this.cursor ) ) { |
111 | | - // TODO: Support tree updates when inserting between elements |
112 | | - } else { |
113 | | - // Get the node we are about to insert into at the lowest depth possible |
114 | | - var node = scope( this.tree.getNodeFromOffset( this.cursor ), op.data ); |
115 | | - if ( !node ) { |
116 | | - throw 'Missing node error. Scope could not be resolved'; |
117 | | - } |
118 | | - // Figure out how deep the data goes |
119 | | - |
120 | | - var offset = this.tree.getOffsetFromNode( node ); |
121 | | - if ( es.DocumentModel.containsElementData( op.data ) ) { |
122 | | - // Perform insert on linear data model |
123 | | - es.insertIntoArray( this.data, this.cursor, op.data ); |
124 | | - annotate.call( this, this.cursor + op.data.length ); |
125 | | - // Synchronize model tree |
126 | | - if ( offset === -1 ) { |
127 | | - throw 'Invalid offset error. Node is not in model tree'; |
128 | | - } |
129 | | - rebuild( |
130 | | - this.data.slice( offset, offset + node.getElementLength() + op.data.length ), |
131 | | - [node] |
132 | | - ); |
133 | | - } else { |
134 | | - // Perform insert on linear data model |
135 | | - // TODO this is duplicated from above |
136 | | - es.insertIntoArray( this.data, this.cursor, op.data ); |
137 | | - annotate.call( this, this.cursor + op.data.length ); |
138 | | - // Update model tree |
139 | | - node.adjustContentLength( op.data.length, true ); |
140 | | - node.emit( 'update', this.cursor - offset ); |
141 | | - } |
142 | | - } |
143 | | - this.cursor += op.data.length; |
144 | | - } |
145 | | - |
146 | | - function remove( op ) { |
147 | | - if ( es.DocumentModel.containsElementData( op.data ) ) { |
148 | | - // Figure out which nodes are covered by the removal |
149 | | - var ranges = this.tree.selectNodes( new es.Range( this.cursor, this.cursor + op.data.length ) ); |
150 | | - var oldNodes = [], newData = [], firstKeptNode = true, lastElement; |
151 | | - for ( var i = 0; i < ranges.length; i++ ) { |
152 | | - oldNodes.push( ranges[i].node ); |
153 | | - if ( ranges[i].globalRange !== undefined ) { |
154 | | - // We have to keep part of this node |
155 | | - if ( firstKeptNode ) { |
156 | | - // This is the first node we're keeping |
157 | | - // Keep its opening as well |
158 | | - newData.push( ranges[i].node.getElement() ); |
159 | | - firstKeptNode = false; |
160 | | - } |
161 | | - |
162 | | - // Compute the start and end offset of this node |
163 | | - // We could do that with getOffsetFromNode() but |
164 | | - // we already have all the numbers we need so why would we |
165 | | - var startOffset = ranges[i].globalRange.start - ranges[i].range.start, |
166 | | - endOffset = startOffset + ranges[i].node.getContentLength(), |
167 | | - // Get this node's data |
168 | | - nodeData = this.data.slice( startOffset, endOffset ); |
169 | | - // Remove data covered by the range from nodeData |
170 | | - nodeData.splice( ranges[i].range.start, ranges[i].range.end - ranges[i].range.start ); |
171 | | - // What remains in nodeData is the data we need to keep |
172 | | - // Append it to newData |
173 | | - newData = newData.concat( nodeData ); |
174 | | - |
175 | | - lastElement = ranges[i].node.getElementType(); |
176 | | - } |
177 | | - } |
178 | | - if ( lastElement !== undefined ) { |
179 | | - // Keep the closing of the last element that was partially kept |
180 | | - newData.push( { 'type': '/' + lastElement } ); |
181 | | - } |
182 | | - |
183 | | - // Perform the rebuild. This updates the model tree |
184 | | - rebuild( newData, oldNodes ); |
185 | | - } else { |
186 | | - // We're removing content only. Take a shortcut |
187 | | - // Get the node we are removing content from |
188 | | - var node = this.tree.getNodeFromOffset( this.cursor ); |
189 | | - // Update model tree |
190 | | - node.adjustContentLength( -op.data.length, true ); |
191 | | - node.emit( 'update', this.cursor - this.tree.getOffsetFromNode( node ) ); |
192 | | - } |
193 | | - |
194 | | - // Update the linear model |
195 | | - this.data.splice( this.cursor, op.data.length ); |
196 | | - } |
197 | | - |
198 | | - function attribute( op, invert ) { |
199 | | - var element = this.data[this.cursor]; |
200 | | - if ( element.type === undefined ) { |
201 | | - throw 'Invalid element error. Can not set attributes on non-element data.'; |
202 | | - } |
203 | | - if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) { |
204 | | - // Automatically initialize attributes object |
205 | | - if ( !element.attributes ) { |
206 | | - element.attributes = {}; |
207 | | - } |
208 | | - element.attributes[op.key] = op.value; |
209 | | - } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) { |
210 | | - if ( element.attributes ) { |
211 | | - delete element.attributes[op.key]; |
212 | | - } |
213 | | - // Automatically clean up attributes object |
214 | | - var empty = true; |
215 | | - for ( var key in element.attributes ) { |
216 | | - empty = false; |
217 | | - break; |
218 | | - } |
219 | | - if ( empty ) { |
220 | | - delete element.attributes; |
221 | | - } |
222 | | - } else { |
223 | | - throw 'Invalid method error. Can not operate attributes this way: ' + method; |
224 | | - } |
225 | | - } |
226 | | - |
227 | | - function annotate( to ) { |
228 | | - var i, |
229 | | - j, |
230 | | - length, |
231 | | - annotation; |
232 | | - // Handle annotations |
233 | | - if ( this.set.length ) { |
234 | | - for ( i = 0, length = this.set.length; i < length; i++ ) { |
235 | | - annotation = this.set[i]; |
236 | | - // Auto-build annotation hash |
237 | | - if ( annotation.hash === undefined ) { |
238 | | - annotation.hash = es.DocumentModel.getAnnotationHash( annotation ); |
239 | | - } |
240 | | - for ( j = this.cursor; j < to; j++ ) { |
241 | | - // Auto-convert to array |
242 | | - if ( es.isArray( this.data[j] ) ) { |
243 | | - this.data[j].push( annotation ); |
244 | | - } else { |
245 | | - this.data[j] = [this.data[j], annotation]; |
246 | | - } |
247 | | - } |
248 | | - } |
249 | | - } |
250 | | - if ( this.clear.length ) { |
251 | | - for ( i = 0, length = this.clear.length; i < length; i++ ) { |
252 | | - annotation = this.clear[i]; |
253 | | - // Auto-build annotation hash |
254 | | - if ( annotation.hash === undefined ) { |
255 | | - annotation.hash = es.DocumentModel.getAnnotationHash( annotation ); |
256 | | - } |
257 | | - for ( j = this.cursor; j < to; j++ ) { |
258 | | - var index = es.DocumentModel.getIndexOfAnnotation( this.data[j], annotation ); |
259 | | - if ( index !== -1 ) { |
260 | | - this.data[j].splice( index, 1 ); |
261 | | - } |
262 | | - // Auto-convert to string |
263 | | - if ( this.data[j].length === 1 ) { |
264 | | - this.data[j] = this.data[j][0]; |
265 | | - } |
266 | | - } |
267 | | - } |
268 | | - } |
269 | | - } |
270 | | - |
271 | | - function mark( op, invert ) { |
272 | | - var target; |
273 | | - if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) { |
274 | | - target = this.set; |
275 | | - } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) { |
276 | | - target = this.clear; |
277 | | - } else { |
278 | | - throw 'Invalid method error. Can not operate attributes this way: ' + method; |
279 | | - } |
280 | | - if ( op.bias === 'start' ) { |
281 | | - target.push( op.annotation ); |
282 | | - } else if ( op.bias === 'stop' ) { |
283 | | - var index = es.DocumentModel.getIndexOfAnnotation( target, op.annotation ); |
284 | | - if ( index === -1 ) { |
285 | | - throw 'Annotation stack error. Annotation is missing.'; |
286 | | - } |
287 | | - target.splice( index, 1 ); |
288 | | - } |
289 | | - } |
290 | | - |
291 | | - return { |
292 | | - // Retain |
293 | | - 'retain': { |
294 | | - 'commit': retain, |
295 | | - 'rollback': retain |
296 | | - }, |
297 | | - // Insert |
298 | | - 'insert': { |
299 | | - 'commit': insert, |
300 | | - 'rollback': remove |
301 | | - }, |
302 | | - // Remove |
303 | | - 'remove': { |
304 | | - 'commit': remove, |
305 | | - 'rollback': insert |
306 | | - }, |
307 | | - // Change element attributes |
308 | | - 'attribute': { |
309 | | - 'commit': function( op ) { |
310 | | - attribute.call( this, op, false ); |
311 | | - }, |
312 | | - 'rollback': function( op ) { |
313 | | - attribute.call( this, op, true ); |
314 | | - } |
315 | | - }, |
316 | | - // Change content annotations |
317 | | - 'annotate': { |
318 | | - 'commit': function( op ) { |
319 | | - mark.call( this, op, false ); |
320 | | - }, |
321 | | - 'rollback': function( op ) { |
322 | | - mark.call( this, op, true ); |
323 | | - } |
324 | | - } |
325 | | - }; |
326 | | -} )(); |
327 | | - |
328 | 63 | /* Static Methods */ |
329 | 64 | |
330 | 65 | /* |
— | — | @@ -1276,22 +1011,7 @@ |
1277 | 1012 | * @param {es.Transaction} |
1278 | 1013 | */ |
1279 | 1014 | es.DocumentModel.prototype.commit = function( transaction ) { |
1280 | | - var state = { |
1281 | | - 'data': this.data, |
1282 | | - 'tree': this, |
1283 | | - 'cursor': 0, |
1284 | | - 'set': [], |
1285 | | - 'clear': [] |
1286 | | - }, |
1287 | | - operations = transaction.getOperations(); |
1288 | | - for ( var i = 0, length = operations.length; i < length; i++ ) { |
1289 | | - var operation = operations[i]; |
1290 | | - if ( operation.type in es.DocumentModel.operations ) { |
1291 | | - es.DocumentModel.operations[operation.type].commit.call( state, operation ); |
1292 | | - } else { |
1293 | | - throw 'Invalid operation error. Operation type is not supported: ' + operation.type; |
1294 | | - } |
1295 | | - } |
| 1015 | + es.TransactionProcessor.commit( this, transaction ); |
1296 | 1016 | }; |
1297 | 1017 | |
1298 | 1018 | /** |
— | — | @@ -1301,22 +1021,7 @@ |
1302 | 1022 | * @param {es.Transaction} |
1303 | 1023 | */ |
1304 | 1024 | es.DocumentModel.prototype.rollback = function( transaction ) { |
1305 | | - var state = { |
1306 | | - 'data': this.data, |
1307 | | - 'tree': this, |
1308 | | - 'cursor': 0, |
1309 | | - 'set': [], |
1310 | | - 'clear': [] |
1311 | | - }, |
1312 | | - operations = transaction.getOperations(); |
1313 | | - for ( var i = 0, length = operations.length; i < length; i++ ) { |
1314 | | - var operation = operations[i]; |
1315 | | - if ( operation.type in es.DocumentModel.operations ) { |
1316 | | - es.DocumentModel.operations[operation.type].rollback.call( state, operation ); |
1317 | | - } else { |
1318 | | - throw 'Invalid operation error. Operation type is not supported: ' + operation.type; |
1319 | | - } |
1320 | | - } |
| 1025 | + es.TransactionProcessor.rollback( this, transaction ); |
1321 | 1026 | }; |
1322 | 1027 | |
1323 | 1028 | /* Inheritance */ |
Index: trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js |
— | — | @@ -0,0 +1,314 @@ |
| 2 | +/** |
| 3 | + * Creates an es.TransactionProcessor object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + */ |
| 8 | +es.TransactionProcessor = function( model, transaction ) { |
| 9 | + this.model = model; |
| 10 | + this.transaction = transaction; |
| 11 | + this.cursor = 0; |
| 12 | + this.set = []; |
| 13 | + this.clear = []; |
| 14 | +}; |
| 15 | + |
| 16 | +/* Static Members */ |
| 17 | + |
| 18 | +es.TransactionProcessor.operationMap = { |
| 19 | + // Retain |
| 20 | + 'retain': { |
| 21 | + 'commit': function( op ) { |
| 22 | + this.retain( op ); |
| 23 | + }, |
| 24 | + 'rollback': function( op ) { |
| 25 | + this.retain( op ); |
| 26 | + } |
| 27 | + }, |
| 28 | + // Insert |
| 29 | + 'insert': { |
| 30 | + 'commit': function( op ) { |
| 31 | + this.insert( op ); |
| 32 | + }, |
| 33 | + 'rollback': function( op ) { |
| 34 | + this.remove( op ); |
| 35 | + } |
| 36 | + }, |
| 37 | + // Remove |
| 38 | + 'remove': { |
| 39 | + 'commit': function( op ) { |
| 40 | + this.remove( op ); |
| 41 | + }, |
| 42 | + 'rollback': function( op ) { |
| 43 | + this.insert( op ); |
| 44 | + } |
| 45 | + }, |
| 46 | + // Change element attributes |
| 47 | + 'attribute': { |
| 48 | + 'commit': function( op ) { |
| 49 | + this.attribute( op, false ); |
| 50 | + }, |
| 51 | + 'rollback': function( op ) { |
| 52 | + this.attribute( op, true ); |
| 53 | + } |
| 54 | + }, |
| 55 | + // Change content annotations |
| 56 | + 'annotate': { |
| 57 | + 'commit': function( op ) { |
| 58 | + this.mark( op, false ); |
| 59 | + }, |
| 60 | + 'rollback': function( op ) { |
| 61 | + this.mark( op, true ); |
| 62 | + } |
| 63 | + } |
| 64 | +}; |
| 65 | + |
| 66 | +/* Static Methods */ |
| 67 | + |
| 68 | +es.TransactionProcessor.commit = function( doc, transaction ) { |
| 69 | + var tp = new es.TransactionProcessor( doc, transaction ); |
| 70 | + tp.process( 'commit' ); |
| 71 | +}; |
| 72 | + |
| 73 | +es.TransactionProcessor.rollback = function( doc, transaction ) { |
| 74 | + var tp = new es.TransactionProcessor( doc, transaction ); |
| 75 | + tp.process( 'rollback' ); |
| 76 | +}; |
| 77 | + |
| 78 | +/* Methods */ |
| 79 | + |
| 80 | +es.TransactionProcessor.prototype.process = function( method ) { |
| 81 | + var operations = this.transaction.getOperations(); |
| 82 | + for ( var i = 0, length = operations.length; i < length; i++ ) { |
| 83 | + var operation = operations[i]; |
| 84 | + if ( operation.type in es.TransactionProcessor.operationMap ) { |
| 85 | + es.TransactionProcessor.operationMap[operation.type][method].call( this, operation ); |
| 86 | + } else { |
| 87 | + throw 'Invalid operation error. Operation type is not supported: ' + operation.type; |
| 88 | + } |
| 89 | + } |
| 90 | +}; |
| 91 | + |
| 92 | +es.TransactionProcessor.prototype.rebuildNodes = function( newData, oldNodes ) { |
| 93 | + var parent, |
| 94 | + index; |
| 95 | + if ( oldNodes[0] === oldNodes[0].getRoot() ) { |
| 96 | + parent = oldNodes[0]; |
| 97 | + parent.splice( 0, parent.getChildren().length ); |
| 98 | + index = 0; |
| 99 | + } else { |
| 100 | + parent = oldNodes[0].getParent(); |
| 101 | + index = parent.indexOf( oldNodes[0] ); |
| 102 | + // Remove the node we are about to insert into from the model tree |
| 103 | + parent.splice( index, oldNodes.length ); |
| 104 | + } |
| 105 | + // Regenerate nodes for the data we've affected |
| 106 | + var newNodes = es.DocumentModel.createNodesFromData( newData ); |
| 107 | + // Insert new elements into the tree where the old ones used to be |
| 108 | + parent.splice.apply( parent, [index, 0].concat( newNodes ) ); |
| 109 | +}; |
| 110 | + |
| 111 | +es.TransactionProcessor.prototype.getScope = function( node, data ) { |
| 112 | + var i, |
| 113 | + length, |
| 114 | + level = 0, |
| 115 | + maxDepth = 0; |
| 116 | + for ( i = 0, length = data.length; i < length; i++ ) { |
| 117 | + if ( typeof data[i].type === 'string' ) { |
| 118 | + level += data[i].type.charAt( 0 ) === '/' ? -1 : 1; |
| 119 | + maxDepth = Math.max( maxDepth, -level ); |
| 120 | + } |
| 121 | + } |
| 122 | + if ( maxDepth > 0 ) { |
| 123 | + for ( i = 0; i < maxDepth; i++ ) { |
| 124 | + node = node.getParent(); |
| 125 | + } |
| 126 | + } |
| 127 | + return node; |
| 128 | +}; |
| 129 | + |
| 130 | +es.TransactionProcessor.prototype.applyAnnotations = function( to ) { |
| 131 | + var i, |
| 132 | + j, |
| 133 | + length, |
| 134 | + annotation; |
| 135 | + // Handle annotations |
| 136 | + if ( this.set.length ) { |
| 137 | + for ( i = 0, length = this.set.length; i < length; i++ ) { |
| 138 | + annotation = this.set[i]; |
| 139 | + // Auto-build annotation hash |
| 140 | + if ( annotation.hash === undefined ) { |
| 141 | + annotation.hash = es.DocumentModel.getAnnotationHash( annotation ); |
| 142 | + } |
| 143 | + for ( j = this.cursor; j < to; j++ ) { |
| 144 | + // Auto-convert to array |
| 145 | + if ( es.isArray( this.model.data[j] ) ) { |
| 146 | + this.model.data[j].push( annotation ); |
| 147 | + } else { |
| 148 | + this.model.data[j] = [this.model.data[j], annotation]; |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + if ( this.clear.length ) { |
| 154 | + for ( i = 0, length = this.clear.length; i < length; i++ ) { |
| 155 | + annotation = this.clear[i]; |
| 156 | + // Auto-build annotation hash |
| 157 | + if ( annotation.hash === undefined ) { |
| 158 | + annotation.hash = es.DocumentModel.getAnnotationHash( annotation ); |
| 159 | + } |
| 160 | + for ( j = this.cursor; j < to; j++ ) { |
| 161 | + var index = es.DocumentModel.getIndexOfAnnotation( this.model.data[j], annotation ); |
| 162 | + if ( index !== -1 ) { |
| 163 | + this.model.data[j].splice( index, 1 ); |
| 164 | + } |
| 165 | + // Auto-convert to string |
| 166 | + if ( this.model.data[j].length === 1 ) { |
| 167 | + this.model.data[j] = this.model.data[j][0]; |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | +}; |
| 173 | + |
| 174 | +es.TransactionProcessor.prototype.retain = function( op ) { |
| 175 | + this.applyAnnotations( this.cursor + op.length ); |
| 176 | + this.cursor += op.length; |
| 177 | +}; |
| 178 | + |
| 179 | +es.TransactionProcessor.prototype.insert = function( op ) { |
| 180 | + if ( es.DocumentModel.isStructuralOffset( this.model.data, this.cursor ) ) { |
| 181 | + // TODO: Support tree updates when inserting between elements |
| 182 | + } else { |
| 183 | + // Get the node we are about to insert into at the lowest depth possible |
| 184 | + var node = this.getScope( this.model.getNodeFromOffset( this.cursor ), op.data ); |
| 185 | + if ( !node ) { |
| 186 | + throw 'Missing node error. Scope could not be resolved'; |
| 187 | + } |
| 188 | + // Figure out how deep the data goes |
| 189 | + |
| 190 | + var offset = this.model.getOffsetFromNode( node ); |
| 191 | + if ( es.DocumentModel.containsElementData( op.data ) ) { |
| 192 | + // Perform insert on linear data model |
| 193 | + es.insertIntoArray( this.model.data, this.cursor, op.data ); |
| 194 | + this.applyAnnotations( this.cursor + op.data.length ); |
| 195 | + // Synchronize model tree |
| 196 | + if ( offset === -1 ) { |
| 197 | + throw 'Invalid offset error. Node is not in model tree'; |
| 198 | + } |
| 199 | + this.rebuildNodes( |
| 200 | + this.model.data.slice( offset, offset + node.getElementLength() + op.data.length ), |
| 201 | + [node] |
| 202 | + ); |
| 203 | + } else { |
| 204 | + // Perform insert on linear data model |
| 205 | + // TODO this is duplicated from above |
| 206 | + es.insertIntoArray( this.model.data, this.cursor, op.data ); |
| 207 | + this.applyAnnotations( this.cursor + op.data.length ); |
| 208 | + // Update model tree |
| 209 | + node.adjustContentLength( op.data.length, true ); |
| 210 | + node.emit( 'update', this.cursor - offset ); |
| 211 | + } |
| 212 | + } |
| 213 | + this.cursor += op.data.length; |
| 214 | +}; |
| 215 | + |
| 216 | +es.TransactionProcessor.prototype.remove = function( op ) { |
| 217 | + if ( es.DocumentModel.containsElementData( op.data ) ) { |
| 218 | + // Figure out which nodes are covered by the removal |
| 219 | + var ranges = this.model.selectNodes( new es.Range( this.cursor, this.cursor + op.data.length ) ); |
| 220 | + var oldNodes = [], newData = [], firstKeptNode = true, lastElement; |
| 221 | + for ( var i = 0; i < ranges.length; i++ ) { |
| 222 | + oldNodes.push( ranges[i].node ); |
| 223 | + if ( ranges[i].globalRange !== undefined ) { |
| 224 | + // We have to keep part of this node |
| 225 | + if ( firstKeptNode ) { |
| 226 | + // This is the first node we're keeping |
| 227 | + // Keep its opening as well |
| 228 | + newData.push( ranges[i].node.getElement() ); |
| 229 | + firstKeptNode = false; |
| 230 | + } |
| 231 | + |
| 232 | + // Compute the start and end offset of this node |
| 233 | + // We could do that with getOffsetFromNode() but |
| 234 | + // we already have all the numbers we need so why would we |
| 235 | + var startOffset = ranges[i].globalRange.start - ranges[i].range.start, |
| 236 | + endOffset = startOffset + ranges[i].node.getContentLength(), |
| 237 | + // Get this node's data |
| 238 | + nodeData = this.model.data.slice( startOffset, endOffset ); |
| 239 | + // Remove data covered by the range from nodeData |
| 240 | + nodeData.splice( ranges[i].range.start, ranges[i].range.end - ranges[i].range.start ); |
| 241 | + // What remains in nodeData is the data we need to keep |
| 242 | + // Append it to newData |
| 243 | + newData = newData.concat( nodeData ); |
| 244 | + |
| 245 | + lastElement = ranges[i].node.getElementType(); |
| 246 | + } |
| 247 | + } |
| 248 | + if ( lastElement !== undefined ) { |
| 249 | + // Keep the closing of the last element that was partially kept |
| 250 | + newData.push( { 'type': '/' + lastElement } ); |
| 251 | + } |
| 252 | + |
| 253 | + // Perform the rebuild. This updates the model tree |
| 254 | + this.rebuildNodes( newData, oldNodes ); |
| 255 | + } else { |
| 256 | + // We're removing content only. Take a shortcut |
| 257 | + // Get the node we are removing content from |
| 258 | + var node = this.model.getNodeFromOffset( this.cursor ); |
| 259 | + // Update model tree |
| 260 | + node.adjustContentLength( -op.data.length, true ); |
| 261 | + node.emit( 'update', this.cursor - this.model.getOffsetFromNode( node ) ); |
| 262 | + } |
| 263 | + |
| 264 | + // Update the linear model |
| 265 | + this.model.data.splice( this.cursor, op.data.length ); |
| 266 | +}; |
| 267 | + |
| 268 | +es.TransactionProcessor.prototype.attribute = function( op, invert ) { |
| 269 | + var element = this.model.data[this.cursor]; |
| 270 | + if ( element.type === undefined ) { |
| 271 | + throw 'Invalid element error. Can not set attributes on non-element data.'; |
| 272 | + } |
| 273 | + if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) { |
| 274 | + // Automatically initialize attributes object |
| 275 | + if ( !element.attributes ) { |
| 276 | + element.attributes = {}; |
| 277 | + } |
| 278 | + element.attributes[op.key] = op.value; |
| 279 | + } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) { |
| 280 | + if ( element.attributes ) { |
| 281 | + delete element.attributes[op.key]; |
| 282 | + } |
| 283 | + // Automatically clean up attributes object |
| 284 | + var empty = true; |
| 285 | + for ( var key in element.attributes ) { |
| 286 | + empty = false; |
| 287 | + break; |
| 288 | + } |
| 289 | + if ( empty ) { |
| 290 | + delete element.attributes; |
| 291 | + } |
| 292 | + } else { |
| 293 | + throw 'Invalid method error. Can not operate attributes this way: ' + method; |
| 294 | + } |
| 295 | +}; |
| 296 | + |
| 297 | +es.TransactionProcessor.prototype.mark = function( op, invert ) { |
| 298 | + var target; |
| 299 | + if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) { |
| 300 | + target = this.set; |
| 301 | + } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) { |
| 302 | + target = this.clear; |
| 303 | + } else { |
| 304 | + throw 'Invalid method error. Can not operate attributes this way: ' + method; |
| 305 | + } |
| 306 | + if ( op.bias === 'start' ) { |
| 307 | + target.push( op.annotation ); |
| 308 | + } else if ( op.bias === 'stop' ) { |
| 309 | + var index = es.DocumentModel.getIndexOfAnnotation( target, op.annotation ); |
| 310 | + if ( index === -1 ) { |
| 311 | + throw 'Annotation stack error. Annotation is missing.'; |
| 312 | + } |
| 313 | + target.splice( index, 1 ); |
| 314 | + } |
| 315 | +}; |
Index: trunk/extensions/VisualEditor/modules/es/bases/es.DocumentBranchNode.js |
— | — | @@ -78,6 +78,9 @@ |
79 | 79 | * @returns {Integer} Offset of node or -1 of node was not found |
80 | 80 | */ |
81 | 81 | es.DocumentBranchNode.prototype.getOffsetFromNode = function( node, shallow ) { |
| 82 | + if ( node === this ) { |
| 83 | + return 0; |
| 84 | + } |
82 | 85 | if ( this.children.length ) { |
83 | 86 | var offset = 0, |
84 | 87 | childNode; |
— | — | @@ -112,6 +115,9 @@ |
113 | 116 | * @returns {es.DocumentNode|null} Node at offset, or null if non was found |
114 | 117 | */ |
115 | 118 | es.DocumentBranchNode.prototype.getNodeFromOffset = function( offset, shallow ) { |
| 119 | + if ( offset === 0 ) { |
| 120 | + return this; |
| 121 | + } |
116 | 122 | // TODO a lot of logic is duplicated in selectNodes(), abstract that into a traverser or something |
117 | 123 | if ( this.children.length ) { |
118 | 124 | var nodeOffset = 0, |