jenkins-bot has submitted this change and it was merged.
Change subject: Prevent deletion of FocusableNodes from a collapsed selection
......................................................................
Prevent deletion of FocusableNodes from a collapsed selection
Instead select the node and require the user to press delete
again if they really meant to delete the node.
Also test cases!
Bug: 55336
Change-Id: I66520e18740e78ce6313f9b31bb575d06b91bea8
---
M VisualEditor.hooks.php
A modules/ve-mw/test/ce/ve.ce.Surface.test.js
M modules/ve-mw/test/dm/ve.dm.mwExample.js
M modules/ve/ce/ve.ce.Surface.js
M modules/ve/test/ce/ve.ce.Surface.test.js
5 files changed, 167 insertions(+), 50 deletions(-)
Approvals:
Catrope: Looks good to me, approved
jenkins-bot: Verified
diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php
index 53c7908..ab501fc 100644
--- a/VisualEditor.hooks.php
+++ b/VisualEditor.hooks.php
@@ -356,6 +356,7 @@
've/test/ce/ve.ce.Document.test.js',
've/test/ce/ve.ce.Surface.test.js',
've-mw/test/ce/ve.ce.Document.test.js',
+ 've-mw/test/ce/ve.ce.Surface.test.js',
've/test/ce/ve.ce.NodeFactory.test.js',
've/test/ce/ve.ce.Node.test.js',
've/test/ce/ve.ce.BranchNode.test.js',
diff --git a/modules/ve-mw/test/ce/ve.ce.Surface.test.js
b/modules/ve-mw/test/ce/ve.ce.Surface.test.js
new file mode 100644
index 0000000..1eb190c
--- /dev/null
+++ b/modules/ve-mw/test/ce/ve.ce.Surface.test.js
@@ -0,0 +1,60 @@
+/*!
+ * VisualEditor ContentEditable Surface tests.
+ *
+ * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+QUnit.module( 've.ce.Surface' );
+
+/* Tests */
+
+QUnit.test( 'handleDelete', function ( assert ) {
+ var i,
+ cases = [
+ {
+ 'html':
+ '<p>Foo</p>' +
+
ve.dm.mwExample.MWTransclusion.blockOpen +
ve.dm.mwExample.MWTransclusion.blockContent +
+ '<p>Bar</p>',
+ 'range': new ve.Range( 4 ),
+ 'operations': ['delete'],
+ 'expectedData': function () {},
+ 'expectedRange': new ve.Range( 5, 7 ),
+ 'msg': 'Block transclusion is focused not
deleted'
+ },
+ {
+ 'html':
+ '<p>Foo</p>' +
+
ve.dm.mwExample.MWTransclusion.blockOpen +
ve.dm.mwExample.MWTransclusion.blockContent +
+ '<p>Bar</p>',
+ 'range': new ve.Range( 4 ),
+ 'operations': ['delete', 'delete'],
+ 'expectedData': function ( data ) {
+ data.splice( 5, 2 );
+ },
+ 'expectedRange': new ve.Range( 5, 5 ),
+ 'msg': 'Block transclusion is deleted with two
keypresses'
+ },
+ {
+ 'html':
+ '<p>Foo</p>' +
+ ve.dm.mwExample.MWBlockImage.html +
+ '<p>Bar</p>',
+ 'range': new ve.Range( 4 ),
+ 'operations': ['delete'],
+ 'expectedData': function () { },
+ 'expectedRange': new ve.Range( 5, 14 ),
+ 'msg': 'Block image is focused not deleted'
+ }
+ ];
+
+ QUnit.expect( cases.length * 2 );
+
+ for ( i = 0; i < cases.length; i++ ) {
+ ve.test.utils.runSurfaceHandleDeleteTest(
+ assert, cases[i].html, cases[i].range,
cases[i].operations,
+ cases[i].expectedData, cases[i].expectedRange,
cases[i].msg
+ );
+ }
+} );
diff --git a/modules/ve-mw/test/dm/ve.dm.mwExample.js
b/modules/ve-mw/test/dm/ve.dm.mwExample.js
index a37350b..fc6939a 100644
--- a/modules/ve-mw/test/dm/ve.dm.mwExample.js
+++ b/modules/ve-mw/test/dm/ve.dm.mwExample.js
@@ -126,6 +126,37 @@
'value': $( ve.dm.mwExample.MWTransclusion.mixed ).toArray()
};
+ve.dm.mwExample.MWBlockImage = {
+ 'html':
+ '<figure typeof="mw:Image/Thumb" class="mw-halign-right
foobar">' +
+ '<a href="Foo"><img src="Bar" width="1" height="2"
resource="FooBar"></a>' +
+ '<figcaption>abc</figcaption>' +
+ '</figure>',
+ 'data': [
+ {
+ 'type': 'mwBlockImage',
+ 'attributes': {
+ 'type': 'thumb',
+ 'align': 'right',
+ 'href': 'Foo',
+ 'src': 'Bar',
+ 'width': '1',
+ 'height': '2',
+ 'resource': 'FooBar',
+ 'originalClasses': 'mw-halign-right foobar',
+ 'unrecognizedClasses': ['foobar']
+ }
+ },
+ { 'type': 'mwImageCaption' },
+ { 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
+ 'a', 'b', 'c',
+ { 'type': '/paragraph' },
+ { 'type': '/mwImageCaption' },
+ { 'type': '/mwBlockImage' }
+ ]
+};
+
+
ve.dm.mwExample.MWReference = {
'referenceList':
'<ol class="references" typeof="mw:Extension/references"
about="#mwt7" data-parsoid="{}"' +
@@ -1613,31 +1644,11 @@
]
},
'thumb image': {
- 'html': '<body><figure typeof="mw:Image/Thumb"
class="mw-halign-right foobar"><a href="Foo"><img src="Bar" width="1"
height="2" resource="FooBar"></a><figcaption>abc</figcaption></figure></body>',
- 'data': [
- {
- 'type': 'mwBlockImage',
- 'attributes': {
- 'type': 'thumb',
- 'align': 'right',
- 'href': 'Foo',
- 'src': 'Bar',
- 'width': '1',
- 'height': '2',
- 'resource': 'FooBar',
- 'originalClasses': 'mw-halign-right
foobar',
- 'unrecognizedClasses': ['foobar']
- }
- },
- { 'type': 'mwImageCaption' },
- { 'type': 'paragraph', 'internal': { 'generated':
'wrapper' } },
- 'a', 'b', 'c',
- { 'type': '/paragraph' },
- { 'type': '/mwImageCaption' },
- { 'type': '/mwBlockImage' },
+ 'html': '<body>' + ve.dm.mwExample.MWBlockImage.html +
'</body>',
+ 'data': ve.dm.mwExample.MWBlockImage.data.concat( [
{ 'type': 'internalList' },
{ 'type': '/internalList' }
- ]
+ ] )
},
'attribute preservation does not crash due to text node split': {
'html':
diff --git a/modules/ve/ce/ve.ce.Surface.js b/modules/ve/ce/ve.ce.Surface.js
index 46aad2d..be9245f 100644
--- a/modules/ve/ce/ve.ce.Surface.js
+++ b/modules/ve/ce/ve.ce.Surface.js
@@ -1512,7 +1512,8 @@
*/
ve.ce.Surface.prototype.handleDelete = function ( e, backspace ) {
var rangeToRemove = this.model.getSelection(),
- tx, startNode, endNode, endNodeData, nodeToDelete;
+ offset = 0,
+ docLength, tx, startNode, endNode, endNodeData, nodeToDelete;
if ( rangeToRemove.isCollapsed() ) {
// In case when the range is collapsed use the same logic that
is used for cursor left and
@@ -1523,6 +1524,20 @@
( e.altKey === true || e.ctrlKey === true ) ? 'word' :
'character',
true
);
+ offset = rangeToRemove.start;
+ docLength = this.model.getDocument().data.getLength();
+ if ( offset < docLength ) {
+ while ( offset < docLength &&
this.model.getDocument().data.isCloseElementData( offset ) ) {
+ offset++;
+ }
+ // If the user tries to delete a focusable node from a
collapsed selection,
+ // just select the node and cancel the deletion.
+ startNode =
this.documentView.getDocumentNode().getNodeFromOffset( offset + 1 );
+ if ( ve.isMixedIn( startNode, ve.ce.FocusableNode ) ) {
+ this.model.change( null,
startNode.getModel().getOuterRange() );
+ return;
+ }
+ }
if ( rangeToRemove.isCollapsed() ) {
// For instance beginning or end of the document.
return;
diff --git a/modules/ve/test/ce/ve.ce.Surface.test.js
b/modules/ve/test/ce/ve.ce.Surface.test.js
index 13093eb..bb51859 100644
--- a/modules/ve/test/ce/ve.ce.Surface.test.js
+++ b/modules/ve/test/ce/ve.ce.Surface.test.js
@@ -9,19 +9,41 @@
/* Tests */
-QUnit.test( 'handleDelete', function ( assert ) {
- var i,
- surface = ve.test.utils.createSurfaceFromHtml(
ve.dm.example.html ),
- view = surface.getView(),
- model = surface.getModel(),
- data = ve.copy( model.getDocument().getFullData() ),
- originalData = ve.copy( data ),
+ve.test.utils.runSurfaceHandleDeleteTest = function( assert, html, range,
operations, expectedData, expectedRange, msg ) {
+ var i, args,
+ selection,
deleteArgs = {
'backspace': [ {}, true ],
'delete': [ {}, false ],
'modifiedBackspace': [ { 'ctrlKey': true }, true ],
'modifiedDelete': [ { 'ctrlKey': true }, false ]
},
+ surface = ve.test.utils.createSurfaceFromHtml( html ||
ve.dm.example.html ),
+ view = surface.getView(),
+ model = surface.getModel(),
+ data = ve.copy( model.getDocument().getFullData() );
+
+ // TODO: model.getSelection() should be consistent after it has been
+ // changed but appears to behave differently depending on the browser.
+ // The selection from the change event is still consistent.
+ model.on( 'change', function( tx, s ) {
+ selection = s;
+ } );
+
+ model.change( null, range );
+ for ( i = 0; i < operations.length; i++ ) {
+ args = deleteArgs[operations[i]];
+ view.handleDelete( args[0], args[1] );
+ }
+ expectedData( data );
+
+ assert.deepEqualWithDomElements( model.getDocument().getFullData(),
data, msg + ': data' );
+ assert.deepEqual( selection, expectedRange, msg + ': range' );
+ surface.destroy();
+};
+
+QUnit.test( 'handleDelete', function ( assert ) {
+ var i,
cases = [
{
'range': new ve.Range( 2 ),
@@ -85,31 +107,39 @@
},
'expectedRange': new ve.Range( 1 ),
'msg': 'Empty node deleted by delete'
+ },
+ {
+ 'range': new ve.Range( 41 ),
+ 'operations': ['backspace'],
+ 'expectedData': function () {},
+ 'expectedRange': new ve.Range( 39, 41 ),
+ 'msg': 'Focusable node selected but not deleted
by backspace'
+ },
+ {
+ 'range': new ve.Range( 39 ),
+ 'operations': ['delete'],
+ 'expectedData': function () {},
+ 'expectedRange': new ve.Range( 39, 41 ),
+ 'msg': 'Focusable node selected but not deleted
by delete'
+ },
+ {
+ 'range': new ve.Range( 39, 41 ),
+ 'operations': ['delete'],
+ 'expectedData': function ( data ) {
+ data.splice( 39, 2 );
+ },
+ 'expectedRange': new ve.Range( 39 ),
+ 'msg': 'Focusable node deleted if selected
first'
}
];
-
- function testRunner( range, operations, expectedData, expectedRange,
msg ) {
- var i, args, data = ve.copy( originalData );
- model.change( null, range );
- for ( i = 0; i < operations.length; i++ ) {
- args = deleteArgs[operations[i]];
- view.handleDelete( args[0], args[1] );
- }
- expectedData( data );
-
- assert.deepEqual( model.getDocument().getFullData(), data, msg
+ ': data' );
- assert.deepEqual( model.getSelection(), expectedRange, msg + ':
range' );
-
- // Roll back the test Surface
- while ( model.undo() ) {
- /*jshint noempty:false */
- }
- }
QUnit.expect( cases.length * 2 );
for ( i = 0; i < cases.length; i++ ) {
- testRunner( cases[i].range, cases[i].operations,
cases[i].expectedData, cases[i].expectedRange, cases[i].msg );
+ ve.test.utils.runSurfaceHandleDeleteTest(
+ assert, cases[i].html, cases[i].range,
cases[i].operations,
+ cases[i].expectedData, cases[i].expectedRange,
cases[i].msg
+ );
}
} );
--
To view, visit https://gerrit.wikimedia.org/r/87666
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I66520e18740e78ce6313f9b31bb575d06b91bea8
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits