Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 17e70c3

Browse files
authored
Merge pull request #1022 from ckeditor/t/1012
Feature: `DataController#deleteContent()` will leave a paragraph if the entire content was selected. Closes #1012. On the occasion `$root` element has been marked as a limit element in Schema in order to simplify the checks.
2 parents 299628b + 757912c commit 17e70c3

File tree

4 files changed

+170
-10
lines changed

4 files changed

+170
-10
lines changed

src/controller/deletecontent.js

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,24 @@ export default function deleteContent( selection, batch, options = {} ) {
3232
return;
3333
}
3434

35-
const selRange = selection.getFirstRange();
35+
// 1. Replace the entire content with paragraph.
36+
// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
37+
if ( shouldEntireContentBeReplacedWithParagraph( batch.document.schema, selection ) ) {
38+
replaceEntireContentWithParagraph( batch, selection );
39+
40+
return;
41+
}
3642

43+
const selRange = selection.getFirstRange();
3744
const startPos = selRange.start;
3845
const endPos = LivePosition.createFromPosition( selRange.end );
3946

40-
// 1. Remove the content if there is any.
47+
// 2. Remove the content if there is any.
4148
if ( !selRange.start.isTouching( selRange.end ) ) {
4249
batch.remove( selRange );
4350
}
4451

45-
// 2. Merge elements in the right branch to the elements in the left branch.
52+
// 3. Merge elements in the right branch to the elements in the left branch.
4653
// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
4754
//
4855
// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
@@ -56,13 +63,10 @@ export default function deleteContent( selection, batch, options = {} ) {
5663

5764
selection.collapse( startPos );
5865

59-
// 3. Autoparagraphing.
66+
// 4. Autoparagraphing.
6067
// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
6168
if ( shouldAutoparagraph( batch.document, startPos ) ) {
62-
const paragraph = new Element( 'paragraph' );
63-
batch.insert( startPos, paragraph );
64-
65-
selection.collapse( paragraph );
69+
insertParagraph( batch, startPos, selection );
6670
}
6771

6872
endPos.detach();
@@ -163,3 +167,61 @@ function checkCanBeMerged( leftPos, rightPos ) {
163167

164168
return true;
165169
}
170+
171+
// Returns the lowest limit element defined in `Schema.limits` for passed selection.
172+
function getLimitElement( schema, selection ) {
173+
let element = selection.getFirstRange().getCommonAncestor();
174+
175+
while ( !schema.limits.has( element.name ) ) {
176+
if ( element.parent ) {
177+
element = element.parent;
178+
} else {
179+
break;
180+
}
181+
}
182+
183+
return element;
184+
}
185+
186+
function insertParagraph( batch, position, selection ) {
187+
const paragraph = new Element( 'paragraph' );
188+
batch.insert( position, paragraph );
189+
190+
selection.collapse( paragraph );
191+
}
192+
193+
function replaceEntireContentWithParagraph( batch, selection ) {
194+
const limitElement = getLimitElement( batch.document.schema, selection );
195+
196+
batch.remove( Range.createIn( limitElement ) );
197+
insertParagraph( batch, Position.createAt( limitElement ), selection );
198+
}
199+
200+
// We want to replace the entire content with a paragraph when:
201+
// * the entire content is selected,
202+
// * selection contains at least two elements,
203+
// * whether the paragraph is allowed in schema in the common ancestor.
204+
function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
205+
const limitElement = getLimitElement( schema, selection );
206+
const limitStartPosition = Position.createAt( limitElement );
207+
const limitEndPosition = Position.createAt( limitElement, 'end' );
208+
209+
if (
210+
!limitStartPosition.isTouching( selection.getFirstPosition() ) ||
211+
!limitEndPosition.isTouching( selection.getLastPosition() )
212+
) {
213+
return false;
214+
}
215+
216+
const range = selection.getFirstRange();
217+
218+
if ( range.start.parent == range.end.parent ) {
219+
return false;
220+
}
221+
222+
if ( !schema.check( { name: 'paragraph', inside: limitElement.name } ) ) {
223+
return false;
224+
}
225+
226+
return true;
227+
}

src/model/schema.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ export default class Schema {
8383
this.allow( { name: '$block', inside: '$root' } );
8484
this.allow( { name: '$inline', inside: '$block' } );
8585

86+
this.limits.add( '$root' );
87+
8688
// TMP!
8789
// Create an "all allowed" context in the schema for processing the pasted content.
8890
// Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588

tests/controller/deletecontent.js

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ describe( 'DataController', () => {
235235

236236
test(
237237
'leaves just one element when all selected',
238-
'<heading1>[x</heading1><paragraph>foo</paragraph><paragraph>y]</paragraph>',
239-
'<heading1>[]</heading1>'
238+
'<heading1>[x</heading1><paragraph>foo</paragraph><paragraph>y]bar</paragraph>',
239+
'<heading1>[]bar</heading1>'
240240
);
241241

242242
it( 'uses remove delta instead of merge delta if merged element is empty', () => {
@@ -450,6 +450,8 @@ describe( 'DataController', () => {
450450

451451
const schema = doc.schema;
452452

453+
schema.limits.add( 'restrictedRoot' );
454+
453455
schema.registerItem( 'image', '$inline' );
454456
schema.registerItem( 'paragraph', '$block' );
455457
schema.registerItem( 'heading1', '$block' );
@@ -465,6 +467,8 @@ describe( 'DataController', () => {
465467
// See also "in simple scenarios => deletes an element".
466468

467469
it( 'deletes two inline elements', () => {
470+
doc.schema.limits.add( 'paragraph' );
471+
468472
setData(
469473
doc,
470474
'x[<image></image><image></image>]z',
@@ -659,6 +663,94 @@ describe( 'DataController', () => {
659663
);
660664
} );
661665

666+
describe( 'should leave a paragraph if the entire content was selected', () => {
667+
beforeEach( () => {
668+
doc = new Document();
669+
doc.createRoot();
670+
671+
const schema = doc.schema;
672+
673+
schema.registerItem( 'div', '$block' );
674+
schema.limits.add( 'div' );
675+
676+
schema.registerItem( 'article', '$block' );
677+
schema.limits.add( 'article' );
678+
679+
schema.registerItem( 'image', '$inline' );
680+
schema.objects.add( 'image' );
681+
682+
schema.registerItem( 'paragraph', '$block' );
683+
schema.registerItem( 'heading1', '$block' );
684+
schema.registerItem( 'heading2', '$block' );
685+
686+
schema.allow( { name: '$text', inside: '$root' } );
687+
688+
schema.allow( { name: 'image', inside: '$root' } );
689+
schema.allow( { name: 'image', inside: 'heading1' } );
690+
schema.allow( { name: 'heading1', inside: 'div' } );
691+
schema.allow( { name: 'paragraph', inside: 'div' } );
692+
schema.allow( { name: 'heading1', inside: 'article' } );
693+
schema.allow( { name: 'heading2', inside: 'article' } );
694+
} );
695+
696+
test(
697+
'but not if only one block was selected',
698+
'<heading1>[xx]</heading1>',
699+
'<heading1>[]</heading1>'
700+
);
701+
702+
test(
703+
'when the entire heading and paragraph were selected',
704+
'<heading1>[xx</heading1><paragraph>yy]</paragraph>',
705+
'<paragraph>[]</paragraph>'
706+
);
707+
708+
test(
709+
'when the entire content was selected',
710+
'<heading1>[x</heading1><paragraph>foo</paragraph><paragraph>y]</paragraph>',
711+
'<paragraph>[]</paragraph>'
712+
);
713+
714+
test(
715+
'inside the limit element when the entire heading and paragraph were inside',
716+
'<div><heading1>[xx</heading1><paragraph>yy]</paragraph></div>',
717+
'<div><paragraph>[]</paragraph></div>'
718+
);
719+
720+
test(
721+
'but not if schema does not accept paragraph in limit element',
722+
'<article><heading1>[xx</heading1><heading2>yy]</heading2></article>',
723+
'<article><heading1>[]</heading1></article>'
724+
);
725+
726+
test(
727+
'but not if selection is not containing the whole content',
728+
'<image></image><heading1>[xx</heading1><paragraph>yy]</paragraph>',
729+
'<image></image><heading1>[]</heading1>'
730+
);
731+
732+
test(
733+
'but not if only single element is selected',
734+
'<heading1>[<image></image>xx]</heading1>',
735+
'<heading1>[]</heading1>'
736+
);
737+
738+
it( 'when root element was not added as Schema.limits works fine as well', () => {
739+
doc.createRoot( 'paragraph', 'paragraphRoot' );
740+
741+
setData(
742+
doc,
743+
'x[<image></image><image></image>]z',
744+
{ rootName: 'paragraphRoot' }
745+
);
746+
747+
deleteContent( doc.selection, doc.batch() );
748+
749+
expect( getData( doc, { rootName: 'paragraphRoot' } ) )
750+
.to.equal( 'x[]z' );
751+
} );
752+
} );
753+
662754
function test( title, input, output, options ) {
663755
it( title, () => {
664756
setData( doc, input );

tests/model/schema/schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ describe( 'Schema', () => {
4949
expect( schema.limits ).to.be.instanceOf( Set );
5050
} );
5151

52+
it( 'should mark $root as a limit element', () => {
53+
expect( schema.limits.has( '$root' ) ).to.be.true;
54+
} );
55+
5256
describe( '$clipboardHolder', () => {
5357
it( 'should allow $block', () => {
5458
expect( schema.check( { name: '$block', inside: [ '$clipboardHolder' ] } ) ).to.be.true;

0 commit comments

Comments
 (0)