From 2768e6ba611f496fc54688aab9a1dc3d4a05015e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 13 Sep 2018 16:29:33 +0200 Subject: [PATCH] Introduce findOptimalInsertionPosition() in utils from image. --- src/utils.js | 50 ++++++++++++++++++++++++++++-- tests/utils.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/utils.js b/src/utils.js index 432b48a0..deb89232 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,7 +8,8 @@ */ import HighlightStack from './highlightstack'; -import Position from '@ckeditor/ckeditor5-engine/src/view/position'; +import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position'; +import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position'; import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; import env from '@ckeditor/ckeditor5-utils/src/env'; @@ -240,6 +241,51 @@ export function toWidgetEditable( editable, writer ) { return editable; } +/** + * Returns a model position which is optimal (in terms of UX) for inserting a widget block. + * + * For instance, if a selection is in the middle of a paragraph, the position before this paragraph + * will be returned so that it is not split. If the selection is at the end of a paragraph, + * the position after this paragraph will be returned. + * + * Note: If the selection is placed in an empty block, that block will be returned. If that position + * is then passed to {@link module:engine/model/model~Model#insertContent}, + * the block will be fully replaced by the image. + * + * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection + * The selection based on which the insertion position should be calculated. + * @returns {module:engine/model/position~Position} The optimal position. + */ +export function findOptimalInsertionPosition( selection ) { + const selectedElement = selection.getSelectedElement(); + + if ( selectedElement ) { + return ModelPosition.createAfter( selectedElement ); + } + + const firstBlock = selection.getSelectedBlocks().next().value; + + if ( firstBlock ) { + // If inserting into an empty block – return position in that block. It will get + // replaced with the image by insertContent(). #42. + if ( firstBlock.isEmpty ) { + return ModelPosition.createAt( firstBlock ); + } + + const positionAfter = ModelPosition.createAfter( firstBlock ); + + // If selection is at the end of the block - return position after the block. + if ( selection.focus.isTouching( positionAfter ) ) { + return positionAfter; + } + + // Otherwise return position before the block. + return ModelPosition.createBefore( firstBlock ); + } + + return selection.focus; +} + // Default filler offset function applied to all widget elements. // // @returns {null} @@ -268,6 +314,6 @@ function addSelectionHandler( editable, writer ) { } ); // Append the selection handler into the widget wrapper. - writer.insert( Position.createAt( editable ), selectionHandler ); + writer.insert( ViewPosition.createAt( editable ), selectionHandler ); writer.addClass( [ 'ck-widget_selectable' ], editable ); } diff --git a/tests/utils.js b/tests/utils.js index 78c2a364..fe1a08e9 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -16,11 +16,14 @@ import { getLabel, toWidgetEditable, setHighlightHandling, + findOptimalInsertionPosition, WIDGET_CLASS_NAME } from '../src/utils'; import UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement'; import env from '@ckeditor/ckeditor5-utils/src/env'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'widget utils', () => { let element, writer, viewDocument; @@ -337,4 +340,85 @@ describe( 'widget utils', () => { expect( addSpy.secondCall.args[ 1 ] ).to.equal( secondDescriptor ); } ); } ); + + describe( 'findOptimalInsertionPosition()', () => { + let model, doc; + + beforeEach( () => { + model = new Model(); + doc = model.document; + + doc.createRoot(); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.register( 'image' ); + model.schema.register( 'span' ); + + model.schema.extend( 'image', { + allowIn: '$root', + isObject: true + } ); + + model.schema.extend( 'span', { allowIn: 'paragraph' } ); + model.schema.extend( '$text', { allowIn: 'span' } ); + } ); + + it( 'returns position after selected element', () => { + setData( model, 'x[]y' ); + + const pos = findOptimalInsertionPosition( doc.selection ); + + expect( pos.path ).to.deep.equal( [ 2 ] ); + } ); + + it( 'returns position inside empty block', () => { + setData( model, 'x[]y' ); + + const pos = findOptimalInsertionPosition( doc.selection ); + + expect( pos.path ).to.deep.equal( [ 1, 0 ] ); + } ); + + it( 'returns position before block if at the beginning of that block', () => { + setData( model, 'x[]fooy' ); + + const pos = findOptimalInsertionPosition( doc.selection ); + + expect( pos.path ).to.deep.equal( [ 1 ] ); + } ); + + it( 'returns position before block if in the middle of that block', () => { + setData( model, 'xf[]ooy' ); + + const pos = findOptimalInsertionPosition( doc.selection ); + + expect( pos.path ).to.deep.equal( [ 1 ] ); + } ); + + it( 'returns position after block if at the end of that block', () => { + setData( model, 'xfoo[]y' ); + + const pos = findOptimalInsertionPosition( doc.selection ); + + expect( pos.path ).to.deep.equal( [ 2 ] ); + } ); + + // Checking if isTouching() was used. + it( 'returns position after block if at the end of that block (deeply nested)', () => { + setData( model, 'xfoobar[]y' ); + + const pos = findOptimalInsertionPosition( doc.selection ); + + expect( pos.path ).to.deep.equal( [ 2 ] ); + } ); + + it( 'returns selection focus if not in a block', () => { + model.schema.extend( '$text', { allowIn: '$root' } ); + setData( model, 'foo[]bar' ); + + const pos = findOptimalInsertionPosition( doc.selection ); + + expect( pos.path ).to.deep.equal( [ 3 ] ); + } ); + } ); } );