Index: trunk/extensions/VisualEditor/tests/es/es.DocumentBranchNode.test.js |
— | — | @@ -948,7 +948,7 @@ |
949 | 949 | ' to ' + selectNodesTests[i].input.end + ')' |
950 | 950 | ); |
951 | 951 | if ( console && console.log && !compare( result, selectNodesTests[i].output ) ) { |
952 | | - console.log("Test " + (i+1) + " FAILED"); |
| 952 | + console.log( "Test " + (i+1) + " FAILED" ); |
953 | 953 | console.log( result ); |
954 | 954 | console.log( selectNodesTests[i].output ); |
955 | 955 | } |
Index: trunk/extensions/VisualEditor/tests/es/es.DocumentModel.test.js |
— | — | @@ -301,7 +301,7 @@ |
302 | 302 | ); |
303 | 303 | } ); |
304 | 304 | |
305 | | -test( 'es.DocumentModel.prepareRemoval', 8, function() { |
| 305 | +test( 'es.DocumentModel.prepareRemoval', 11, function() { |
306 | 306 | var documentModel = es.DocumentModel.newFromPlainObject( esTest.obj ); |
307 | 307 | |
308 | 308 | // Test 1 |
— | — | @@ -341,26 +341,9 @@ |
342 | 342 | ], |
343 | 343 | 'prepareRemoval removes entire elements' |
344 | 344 | ); |
345 | | - |
| 345 | + |
346 | 346 | // Test 3 |
347 | 347 | deepEqual( |
348 | | - documentModel.prepareRemoval( new es.Range( 21, 23 ) ).getOperations(), |
349 | | - [ |
350 | | - { 'type': 'retain', 'length': 21 }, |
351 | | - { |
352 | | - 'type': 'remove', |
353 | | - 'data': [ |
354 | | - { 'type': '/listItem' }, |
355 | | - { 'type': 'listItem', 'attributes': { 'styles': ['number'] } } |
356 | | - ] |
357 | | - }, |
358 | | - { 'type': 'retain', 'length': 11 } |
359 | | - ], |
360 | | - 'prepareRemoval merges two list items' |
361 | | - ); |
362 | | - |
363 | | - // Test 4 |
364 | | - deepEqual( |
365 | 348 | documentModel.prepareRemoval( new es.Range( 3, 9 ) ).getOperations(), |
366 | 349 | [ |
367 | 350 | { 'type': 'retain', 'length': 3 }, |
— | — | @@ -375,7 +358,7 @@ |
376 | 359 | 'prepareRemoval works across structural nodes' |
377 | 360 | ); |
378 | 361 | |
379 | | - // Test 5 |
| 362 | + // Test 4 |
380 | 363 | deepEqual( |
381 | 364 | documentModel.prepareRemoval( new es.Range( 3, 24 ) ).getOperations(), |
382 | 365 | [ |
— | — | @@ -410,7 +393,7 @@ |
411 | 394 | 'prepareRemoval strips and drops correctly when working across structural nodes' |
412 | 395 | ); |
413 | 396 | |
414 | | - // Test 6 |
| 397 | + // Test 5 |
415 | 398 | deepEqual( |
416 | 399 | documentModel.prepareRemoval( new es.Range( 3, 25 ) ).getOperations(), |
417 | 400 | [ |
— | — | @@ -450,7 +433,7 @@ |
451 | 434 | 'prepareRemoval strips and drops correctly when working across structural nodes (2)' |
452 | 435 | ); |
453 | 436 | |
454 | | - // Test 7 |
| 437 | + // Test 6 |
455 | 438 | deepEqual( |
456 | 439 | documentModel.prepareRemoval( new es.Range( 9, 17 ) ).getOperations(), |
457 | 440 | [ |
— | — | @@ -475,7 +458,7 @@ |
476 | 459 | 'prepareRemoval will not merge items of unequal types' |
477 | 460 | ); |
478 | 461 | |
479 | | - // Test 8 |
| 462 | + // Test 7 |
480 | 463 | deepEqual( |
481 | 464 | documentModel.prepareRemoval( new es.Range( 9, 27 ) ).getOperations(), |
482 | 465 | [ |
— | — | @@ -509,6 +492,76 @@ |
510 | 493 | ], |
511 | 494 | 'prepareRemoval blanks a paragraph and a list' |
512 | 495 | ); |
| 496 | + |
| 497 | + // Test 8 |
| 498 | + deepEqual( |
| 499 | + documentModel.prepareRemoval( new es.Range( 21, 23 ) ).getOperations(), |
| 500 | + [ |
| 501 | + { 'type': 'retain', 'length': 21 }, |
| 502 | + { |
| 503 | + 'type': 'remove', |
| 504 | + 'data': [ |
| 505 | + { 'type': '/listItem' }, |
| 506 | + { 'type': 'listItem', 'attributes': { 'styles': ['number'] } } |
| 507 | + ] |
| 508 | + }, |
| 509 | + { 'type': 'retain', 'length': 11 } |
| 510 | + ], |
| 511 | + 'prepareRemoval merges two list items' |
| 512 | + ); |
| 513 | + |
| 514 | + // Test 9 |
| 515 | + deepEqual( |
| 516 | + documentModel.prepareRemoval( new es.Range( 20, 24 ) ).getOperations(), |
| 517 | + [ |
| 518 | + { 'type': 'retain', 'length': 20 }, |
| 519 | + { |
| 520 | + 'type': 'remove', |
| 521 | + 'data': [ |
| 522 | + { 'type': '/paragraph' }, |
| 523 | + { 'type': '/listItem' }, |
| 524 | + { 'type': 'listItem', 'attributes': { 'styles': ['number'] } }, |
| 525 | + { 'type': 'paragraph' } |
| 526 | + ] |
| 527 | + }, |
| 528 | + { 'type': 'retain', 'length': 10 } |
| 529 | + ], |
| 530 | + 'prepareRemoval merges two list items and the paragraphs inside them' |
| 531 | + ); |
| 532 | + |
| 533 | + // Test 10 |
| 534 | + deepEqual( |
| 535 | + documentModel.prepareRemoval( new es.Range( 20, 23 ) ).getOperations(), |
| 536 | + [ |
| 537 | + { 'type': 'retain', 'length': 34 } |
| 538 | + ], |
| 539 | + 'prepareRemoval returns a null transaction when attempting an unbalanced merge' |
| 540 | + ); |
| 541 | + |
| 542 | + // Test 11 |
| 543 | + deepEqual( |
| 544 | + documentModel.prepareRemoval( new es.Range( 15, 24 ) ).getOperations(), |
| 545 | + [ |
| 546 | + { 'type': 'retain', 'length': 15 }, |
| 547 | + { |
| 548 | + 'type': 'remove', |
| 549 | + 'data': [ |
| 550 | + { 'type': '/paragraph' }, |
| 551 | + { 'type': '/listItem' }, |
| 552 | + { 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } }, |
| 553 | + { 'type': 'paragraph' }, |
| 554 | + 'f', |
| 555 | + { 'type': '/paragraph' }, |
| 556 | + { 'type': '/listItem' }, |
| 557 | + { 'type': 'listItem', 'attributes': { 'styles': ['number'] } }, |
| 558 | + { 'type': 'paragraph' } |
| 559 | + ] |
| 560 | + }, |
| 561 | + { 'type': 'retain', 'length': 10 } |
| 562 | + ], |
| 563 | + 'prepareRemoval merges two list items and the paragraphs inside them' |
| 564 | + ); |
| 565 | + |
513 | 566 | } ); |
514 | 567 | |
515 | 568 | test( 'es.DocumentModel.prepareInsertion', 11, function() { |
Index: trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js |
— | — | @@ -827,6 +827,36 @@ |
828 | 828 | */ |
829 | 829 | |
830 | 830 | es.DocumentModel.prototype.prepareRemoval = function( range ) { |
| 831 | + // If a selection is painted across two paragraphs, and then the text is deleted, the two |
| 832 | + // paragraphs can become one paragraph. However, if the selection crosses into a table, those |
| 833 | + // cannot be merged. To make this simple, we follow rule #2 in the comment above for deciding |
| 834 | + // whether two elements can be merged. |
| 835 | + // So you can merge adjacent paragraphs, or list items. And you can't merge a paragraph into |
| 836 | + // a table row. There may be other rules we will want in here later, for instance, special |
| 837 | + // casing merging a listitem into a paragraph. |
| 838 | + function canMerge( node1, node2 ) { |
| 839 | + var n1 = node1, n2 = node2; |
| 840 | + // Simultaneously walk upwards from node1 and node2 |
| 841 | + // until we reach the common ancestor. |
| 842 | + while ( n1 !== n2 ) { |
| 843 | + if ( n1.getElementType() !== n2.getElementType() ) { |
| 844 | + // Not the same type |
| 845 | + return false; |
| 846 | + } |
| 847 | + n1 = n1.getParent(); |
| 848 | + n2 = n2.getParent(); |
| 849 | + if ( n1 === null || n2 === null ) { |
| 850 | + // Reached a root, so no common ancestor |
| 851 | + // or different depth |
| 852 | + return false; |
| 853 | + } |
| 854 | + } |
| 855 | + // We've reached the common ancestor using simultaneous traversal, |
| 856 | + // so we know node1 and node2 have the same depth. We also haven't |
| 857 | + // seen any nodes with mismatching types along the way, so we're good. |
| 858 | + return true; |
| 859 | + } |
| 860 | + |
831 | 861 | var tx = new es.Transaction(), selectedNodes, selectedNode, startNode, endNode, i; |
832 | 862 | range.normalize(); |
833 | 863 | if ( range.start === range.end ) { |
— | — | @@ -840,17 +870,7 @@ |
841 | 871 | startNode = selectedNodes[0].node; |
842 | 872 | endNode = selectedNodes[selectedNodes.length - 1].node; |
843 | 873 | |
844 | | - // If a selection is painted across two paragraphs, and then the text is deleted, the two |
845 | | - // paragraphs can become one paragraph. However, if the selection crosses into a table, those |
846 | | - // cannot be merged. To make this simple, we are follow a basic rule: |
847 | | - // can merge = ( same type ) && ( same parent ) |
848 | | - // So you can merge adjacent paragraphs, or listitems. And you can't merge a paragraph into |
849 | | - // a table row. There may be other rules we will want in here later, for instance, special |
850 | | - // casing merging a listitem into a paragraph. |
851 | | - if ( startNode && endNode && |
852 | | - startNode.getParent() === endNode.getParent() && |
853 | | - startNode.getElementType() === endNode.getElementType() |
854 | | - ) { |
| 874 | + if ( startNode && endNode && canMerge( startNode, endNode ) ) { |
855 | 875 | // This is the simple case. node1 and node2 are either the same node, or can be merged |
856 | 876 | // So we can just remove all the data in the range and call it a day, no fancy |
857 | 877 | // processing necessary |