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

Commit

Permalink
Merge branch 'master' into t/1551
Browse files Browse the repository at this point in the history
  • Loading branch information
f1ames committed Jan 3, 2019
2 parents 7b41748 + 0e0cae6 commit 1649b5f
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 34 deletions.
41 changes: 13 additions & 28 deletions src/conversion/conversion.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default class Conversion {
* @private
* @member {Map}
*/
this._dispatchersGroups = new Map();
this._conversionHelpers = new Map();
}

/**
Expand All @@ -70,17 +70,12 @@ export default class Conversion {
* If a given group name is used for the second time, the
* {@link module:utils/ckeditorerror~CKEditorError `conversion-register-group-exists` error} is thrown.
*
* @param {Object} options
* @param {String} options.name The name for dispatchers group.
* @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
* module:engine/conversion/upcastdispatcher~UpcastDispatcher|Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
* module:engine/conversion/upcastdispatcher~UpcastDispatcher>} options.dispatcher Dispatcher or array of dispatchers to register
* under the given name.
* @param {String} name The name for dispatchers group.
* @param {module:engine/conversion/downcasthelpers~DowncastHelpers|
* module:engine/conversion/upcasthelpers~UpcastHelpers} helpers
* module:engine/conversion/upcasthelpers~UpcastHelpers} conversionHelpers
*/
register( name, group ) {
if ( this._dispatchersGroups.has( name ) ) {
register( name, conversionHelpers ) {
if ( this._conversionHelpers.has( name ) ) {
/**
* Trying to register a group name that was already registered.
*
Expand All @@ -89,7 +84,7 @@ export default class Conversion {
throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' );
}

this._dispatchersGroups.set( name, group );
this._conversionHelpers.set( name, conversionHelpers );
}

/**
Expand Down Expand Up @@ -138,9 +133,7 @@ export default class Conversion {
* @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
for( groupName ) {
const group = this._getDispatchersGroup( groupName );

return group;
return this._getConversionHelpers( groupName );
}

/**
Expand Down Expand Up @@ -396,7 +389,7 @@ export default class Conversion {
.elementToAttribute( {
view,
model,
converterPriority: definition.priority
converterPriority: definition.converterPriority
} );
}
}
Expand Down Expand Up @@ -526,17 +519,17 @@ export default class Conversion {
}

/**
* Returns dispatchers group registered under a given group name.
* Returns conversion helpers registered under a given name.
*
* If the given group name has not been registered, the
* {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown.
*
* @private
* @param {String} groupName
* @returns {module:engine/conversion/conversion~DispatchersGroup}
* @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
_getDispatchersGroup( groupName ) {
if ( !this._dispatchersGroups.has( groupName ) ) {
_getConversionHelpers( groupName ) {
if ( !this._conversionHelpers.has( groupName ) ) {
/**
* Trying to add a converter to an unknown dispatchers group.
*
Expand All @@ -545,7 +538,7 @@ export default class Conversion {
throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
}

return this._dispatchersGroups.get( groupName );
return this._conversionHelpers.get( groupName );
}
}

Expand All @@ -566,14 +559,6 @@ export default class Conversion {
* @property {module:utils/priorities~PriorityString} [converterPriority] The converter priority.
*/

/**
* @typedef {Object} module:engine/conversion/conversion~DispatchersGroup
* @property {String} name Group name
* @property {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
* module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
* @property {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers} helpers
*/

// Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
// `definition.upcastAlso`.
//
Expand Down
22 changes: 21 additions & 1 deletion src/model/documentselection.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,32 @@ export default class DocumentSelection {
* <paragraph>b</paragraph>
* <paragraph>]c</paragraph> // this block will not be returned
*
* @returns {Iterator.<module:engine/model/element~Element>}
* @returns {Iterable.<module:engine/model/element~Element>}
*/
getSelectedBlocks() {
return this._selection.getSelectedBlocks();
}

/**
* Returns blocks that aren't nested in other selected blocks.
*
* In this case the method will return blocks A, B and E because C & D are children of block B:
*
* [<blockA></blockA>
* <blockB>
* <blockC></blockC>
* <blockD></blockD>
* </blockB>
* <blockE></blockE>]
*
* **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
*
* @returns {Iterable.<module:engine/model/element~Element>}
*/
getTopMostBlocks() {
return this._selection.getTopMostBlocks();
}

/**
* Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
* one range in the selection, and that range contains exactly one element.
Expand Down
6 changes: 3 additions & 3 deletions src/model/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ export default class Schema {
*
* @param {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
* @param {String} attribute The name of the attribute to check.
* @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
* @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
*/
* getValidRanges( ranges, attribute ) {
ranges = convertToMinimalFlatRanges( ranges );
Expand All @@ -539,7 +539,7 @@ export default class Schema {
* @private
* @param {module:engine/model/range~Range} range Range to process.
* @param {String} attribute The name of the attribute to check.
* @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
* @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
*/
* _getValidRangesForRange( range, attribute ) {
let start = range.start;
Expand Down Expand Up @@ -1459,7 +1459,7 @@ function* combineWalkers( backward, forward ) {
// all those minimal flat ranges.
//
// @param {Array.<module:engine/model/range~Range>} ranges Ranges to process.
// @returns {Iterator.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
// @returns {Iterable.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
function* convertToMinimalFlatRanges( ranges ) {
for ( const range of ranges ) {
yield* range.getMinimalFlatRanges();
Expand Down
47 changes: 47 additions & 0 deletions src/model/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,35 @@ export default class Selection {
}
}

/**
* Returns blocks that aren't nested in other selected blocks.
*
* In this case the method will return blocks A, B and E because C & D are children of block B:
*
* [<blockA></blockA>
* <blockB>
* <blockC></blockC>
* <blockD></blockD>
* </blockB>
* <blockE></blockE>]
*
* **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
*
* @returns {Iterable.<module:engine/model/element~Element>}
*/
* getTopMostBlocks() {
const selected = Array.from( this.getSelectedBlocks() );

for ( const block of selected ) {
const parentBlock = findAncestorBlock( block );

// Filter out blocks that are nested in other selected blocks (like paragraphs in tables).
if ( !parentBlock || !selected.includes( parentBlock ) ) {
yield block;
}
}
}

/**
* Checks whether the selection contains the entire content of the given element. This means that selection must start
* at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
Expand Down Expand Up @@ -799,6 +828,24 @@ function getParentBlock( position, visited ) {
return block;
}

// Returns first ancestor block of a node.
//
// @param {module:engine/model/node~Node} node
// @returns {module:engine/model/node~Node|undefined}
function findAncestorBlock( node ) {
const schema = node.document.model.schema;

let parent = node.parent;

while ( parent ) {
if ( schema.isBlock( parent ) ) {
return parent;
}

parent = parent.parent;
}
}

/**
* An entity that is used to set selection.
*
Expand Down
39 changes: 37 additions & 2 deletions tests/conversion/conversion.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,24 @@ describe( 'Conversion', () => {
test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
} );

it( 'config.converterPriority is defined', () => {
it( 'config.converterPriority is defined (override downcast)', () => {
conversion.elementToElement( { model: 'paragraph', view: 'p' } );
conversion.elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } );

test( '<div>Foo</div>', '<paragraph>Foo</paragraph>' );
test( '<p>Foo</p>', '<paragraph>Foo</paragraph>', '<div>Foo</div>' );
} );

it( 'config.converterPriority is defined (override upcast)', () => {
schema.register( 'foo', {
inheritAllFrom: '$block'
} );
conversion.elementToElement( { model: 'paragraph', view: 'p' } );
conversion.elementToElement( { model: 'foo', view: 'p', converterPriority: 'high' } );

test( '<p>Foo</p>', '<foo>Foo</foo>', '<p>Foo</p>' );
} );

it( 'config.view is an object', () => {
schema.register( 'fancyParagraph', {
inheritAllFrom: 'paragraph'
Expand Down Expand Up @@ -232,14 +242,28 @@ describe( 'Conversion', () => {
test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
} );

it( 'config.converterPriority is defined', () => {
it( 'config.converterPriority is defined (override downcast)', () => {
conversion.attributeToElement( { model: 'bold', view: 'strong' } );
conversion.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );

test( '<p><b>Foo</b></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>' );
test( '<p><strong>Foo</strong></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>', '<p><b>Foo</b></p>' );
} );

it( 'config.converterPriority is defined (override upcast)', () => {
schema.extend( '$text', {
allowAttributes: [ 'foo' ]
} );
conversion.attributeToElement( { model: 'bold', view: 'strong' } );
conversion.attributeToElement( { model: 'foo', view: 'strong', converterPriority: 'high' } );

test(
'<p><strong>Foo</strong></p>',
'<paragraph><$text foo="true">Foo</$text></paragraph>',
'<p><strong>Foo</strong></p>'
);
} );

it( 'config.view is an object', () => {
conversion.attributeToElement( {
model: 'bold',
Expand Down Expand Up @@ -634,6 +658,17 @@ describe( 'Conversion', () => {
'<div border="border"><div shade="shade"></div></div>'
);
} );

it( 'config.converterPriority is defined (override downcast)', () => {
schema.extend( 'image', {
allowAttributes: [ 'foo' ]
} );

conversion.attributeToAttribute( { model: 'foo', view: 'foo' } );
conversion.attributeToAttribute( { model: 'foo', view: 'foofoo', converterPriority: 'high' } );

test( '<img foo="foo"></img>', '<image foo="foo"></image>', '<img foofoo="foo"></img>' );
} );
} );

function test( input, expectedModel, expectedView = null ) {
Expand Down
68 changes: 68 additions & 0 deletions tests/model/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,74 @@ describe( 'Selection', () => {
}
} );

describe( 'getTopMostBlocks()', () => {
beforeEach( () => {
model.schema.register( 'p', { inheritAllFrom: '$block' } );
model.schema.register( 'lvl0', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
model.schema.register( 'lvl1', { allowIn: 'lvl0', isLimit: true } );
model.schema.register( 'lvl2', { allowIn: 'lvl1', isObject: true } );

model.schema.extend( 'p', { allowIn: 'lvl2' } );
} );

it( 'returns an iterator', () => {
setData( model, '<p>a</p><p>[]b</p><p>c</p>' );

expect( doc.selection.getTopMostBlocks().next ).to.be.a( 'function' );
} );

it( 'returns block for a collapsed selection', () => {
setData( model, '<p>a</p><p>[]b</p><p>c</p>' );

expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
} );

it( 'returns block for a collapsed selection (empty block)', () => {
setData( model, '<p>a</p><p>[]</p><p>c</p>' );

const blocks = Array.from( doc.selection.getTopMostBlocks() );

expect( blocks ).to.have.length( 1 );
expect( blocks[ 0 ].childCount ).to.equal( 0 );
} );

it( 'returns block for a non collapsed selection', () => {
setData( model, '<p>a</p><p>[b]</p><p>c</p>' );

expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
} );

it( 'returns two blocks for a non collapsed selection', () => {
setData( model, '<p>a</p><p>[b</p><p>c]</p><p>d</p>' );

expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b', 'p#c' ] );
} );

it( 'returns only top most blocks', () => {
setData( model, '[<p>foo</p><lvl0><lvl1><lvl2><p>bar</p></lvl2></lvl1></lvl0><p>baz</p>]' );

expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#foo', 'lvl0', 'p#baz' ] );
} );

it( 'returns only selected blocks even if nested in other blocks', () => {
setData( model, '<p>foo</p><lvl0><lvl1><lvl2><p>[b]ar</p></lvl2></lvl1></lvl0><p>baz</p>' );

expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#bar' ] );
} );

// Map all elements to names. If element contains child text node it will be appended to name with '#'.
function stringifyBlocks( elements ) {
return Array.from( elements ).map( el => {
const name = el.name;

const firstChild = el.getChild( 0 );
const hasText = firstChild && firstChild.data;

return hasText ? `${ name }#${ firstChild.data }` : name;
} );
}
} );

describe( 'attributes interface', () => {
let rangeInFullP;

Expand Down

0 comments on commit 1649b5f

Please sign in to comment.