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

Commit 9c0d4ce

Browse files
author
Piotr Jasiun
authored
Merge pull request #53 from ckeditor/t/ckeditor5/1243
Feature: Introduce findOptimalInsertionPostion() utility method.
2 parents 90ea50d + 2768e6b commit 9c0d4ce

File tree

2 files changed

+132
-2
lines changed

2 files changed

+132
-2
lines changed

src/utils.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
*/
99

1010
import HighlightStack from './highlightstack';
11-
import Position from '@ckeditor/ckeditor5-engine/src/view/position';
11+
import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
12+
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
1213
import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview';
1314
import env from '@ckeditor/ckeditor5-utils/src/env';
1415

@@ -240,6 +241,51 @@ export function toWidgetEditable( editable, writer ) {
240241
return editable;
241242
}
242243

244+
/**
245+
* Returns a model position which is optimal (in terms of UX) for inserting a widget block.
246+
*
247+
* For instance, if a selection is in the middle of a paragraph, the position before this paragraph
248+
* will be returned so that it is not split. If the selection is at the end of a paragraph,
249+
* the position after this paragraph will be returned.
250+
*
251+
* Note: If the selection is placed in an empty block, that block will be returned. If that position
252+
* is then passed to {@link module:engine/model/model~Model#insertContent},
253+
* the block will be fully replaced by the image.
254+
*
255+
* @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
256+
* The selection based on which the insertion position should be calculated.
257+
* @returns {module:engine/model/position~Position} The optimal position.
258+
*/
259+
export function findOptimalInsertionPosition( selection ) {
260+
const selectedElement = selection.getSelectedElement();
261+
262+
if ( selectedElement ) {
263+
return ModelPosition.createAfter( selectedElement );
264+
}
265+
266+
const firstBlock = selection.getSelectedBlocks().next().value;
267+
268+
if ( firstBlock ) {
269+
// If inserting into an empty block – return position in that block. It will get
270+
// replaced with the image by insertContent(). #42.
271+
if ( firstBlock.isEmpty ) {
272+
return ModelPosition.createAt( firstBlock );
273+
}
274+
275+
const positionAfter = ModelPosition.createAfter( firstBlock );
276+
277+
// If selection is at the end of the block - return position after the block.
278+
if ( selection.focus.isTouching( positionAfter ) ) {
279+
return positionAfter;
280+
}
281+
282+
// Otherwise return position before the block.
283+
return ModelPosition.createBefore( firstBlock );
284+
}
285+
286+
return selection.focus;
287+
}
288+
243289
// Default filler offset function applied to all widget elements.
244290
//
245291
// @returns {null}
@@ -268,6 +314,6 @@ function addSelectionHandler( editable, writer ) {
268314
} );
269315

270316
// Append the selection handler into the widget wrapper.
271-
writer.insert( Position.createAt( editable ), selectionHandler );
317+
writer.insert( ViewPosition.createAt( editable ), selectionHandler );
272318
writer.addClass( [ 'ck-widget_selectable' ], editable );
273319
}

tests/utils.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ import {
1616
getLabel,
1717
toWidgetEditable,
1818
setHighlightHandling,
19+
findOptimalInsertionPosition,
1920
WIDGET_CLASS_NAME
2021
} from '../src/utils';
2122
import UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
2223
import env from '@ckeditor/ckeditor5-utils/src/env';
2324
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
25+
import Model from '@ckeditor/ckeditor5-engine/src/model/model';
26+
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
2427

2528
describe( 'widget utils', () => {
2629
let element, writer, viewDocument;
@@ -337,4 +340,85 @@ describe( 'widget utils', () => {
337340
expect( addSpy.secondCall.args[ 1 ] ).to.equal( secondDescriptor );
338341
} );
339342
} );
343+
344+
describe( 'findOptimalInsertionPosition()', () => {
345+
let model, doc;
346+
347+
beforeEach( () => {
348+
model = new Model();
349+
doc = model.document;
350+
351+
doc.createRoot();
352+
353+
model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
354+
model.schema.register( 'image' );
355+
model.schema.register( 'span' );
356+
357+
model.schema.extend( 'image', {
358+
allowIn: '$root',
359+
isObject: true
360+
} );
361+
362+
model.schema.extend( 'span', { allowIn: 'paragraph' } );
363+
model.schema.extend( '$text', { allowIn: 'span' } );
364+
} );
365+
366+
it( 'returns position after selected element', () => {
367+
setData( model, '<paragraph>x</paragraph>[<image></image>]<paragraph>y</paragraph>' );
368+
369+
const pos = findOptimalInsertionPosition( doc.selection );
370+
371+
expect( pos.path ).to.deep.equal( [ 2 ] );
372+
} );
373+
374+
it( 'returns position inside empty block', () => {
375+
setData( model, '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );
376+
377+
const pos = findOptimalInsertionPosition( doc.selection );
378+
379+
expect( pos.path ).to.deep.equal( [ 1, 0 ] );
380+
} );
381+
382+
it( 'returns position before block if at the beginning of that block', () => {
383+
setData( model, '<paragraph>x</paragraph><paragraph>[]foo</paragraph><paragraph>y</paragraph>' );
384+
385+
const pos = findOptimalInsertionPosition( doc.selection );
386+
387+
expect( pos.path ).to.deep.equal( [ 1 ] );
388+
} );
389+
390+
it( 'returns position before block if in the middle of that block', () => {
391+
setData( model, '<paragraph>x</paragraph><paragraph>f[]oo</paragraph><paragraph>y</paragraph>' );
392+
393+
const pos = findOptimalInsertionPosition( doc.selection );
394+
395+
expect( pos.path ).to.deep.equal( [ 1 ] );
396+
} );
397+
398+
it( 'returns position after block if at the end of that block', () => {
399+
setData( model, '<paragraph>x</paragraph><paragraph>foo[]</paragraph><paragraph>y</paragraph>' );
400+
401+
const pos = findOptimalInsertionPosition( doc.selection );
402+
403+
expect( pos.path ).to.deep.equal( [ 2 ] );
404+
} );
405+
406+
// Checking if isTouching() was used.
407+
it( 'returns position after block if at the end of that block (deeply nested)', () => {
408+
setData( model, '<paragraph>x</paragraph><paragraph>foo<span>bar[]</span></paragraph><paragraph>y</paragraph>' );
409+
410+
const pos = findOptimalInsertionPosition( doc.selection );
411+
412+
expect( pos.path ).to.deep.equal( [ 2 ] );
413+
} );
414+
415+
it( 'returns selection focus if not in a block', () => {
416+
model.schema.extend( '$text', { allowIn: '$root' } );
417+
setData( model, 'foo[]bar' );
418+
419+
const pos = findOptimalInsertionPosition( doc.selection );
420+
421+
expect( pos.path ).to.deep.equal( [ 3 ] );
422+
} );
423+
} );
340424
} );

0 commit comments

Comments
 (0)