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

Unify widget insertion #53

Merged
merged 1 commit into from
Sep 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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 );
}
84 changes: 84 additions & 0 deletions tests/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, '<paragraph>x</paragraph>[<image></image>]<paragraph>y</paragraph>' );

const pos = findOptimalInsertionPosition( doc.selection );

expect( pos.path ).to.deep.equal( [ 2 ] );
} );

it( 'returns position inside empty block', () => {
setData( model, '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );

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, '<paragraph>x</paragraph><paragraph>[]foo</paragraph><paragraph>y</paragraph>' );

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, '<paragraph>x</paragraph><paragraph>f[]oo</paragraph><paragraph>y</paragraph>' );

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, '<paragraph>x</paragraph><paragraph>foo[]</paragraph><paragraph>y</paragraph>' );

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, '<paragraph>x</paragraph><paragraph>foo<span>bar[]</span></paragraph><paragraph>y</paragraph>' );

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 ] );
} );
} );
} );