@ckeditor/ckeditor5-utils
Advanced tools
Comparing version 0.8.0 to 0.9.0
Changelog | ||
========= | ||
## [0.9.0](https://github.com/ckeditor/ckeditor5-utils/compare/v0.8.0...v0.9.0) (2017-04-05) | ||
### Bug fixes | ||
* The `getOptimalPosition()` utility should work fine when the parent element has a scroll. Closes [#139](https://github.com/ckeditor/ckeditor5-utils/issues/139). ([b878949](https://github.com/ckeditor/ckeditor5-utils/commit/b878949)) | ||
### Features | ||
* `Collection.bindTo()` method now is not only available in the `ViewCollection` but in all `Collection`s. Closes [#125](https://github.com/ckeditor/ckeditor5-utils/issues/125). ([4e299be](https://github.com/ckeditor/ckeditor5-utils/commit/4e299be)) | ||
* Added the `first()` function. Closes [#130](https://github.com/ckeditor/ckeditor5-utils/issues/130). ([8ab07d2](https://github.com/ckeditor/ckeditor5-utils/commit/8ab07d2)) | ||
* Two–way data binding between `Collection` instances. Closes [#132](https://github.com/ckeditor/ckeditor5-utils/issues/132). ([6b79624](https://github.com/ckeditor/ckeditor5-utils/commit/6b79624)) | ||
## [0.8.0](https://github.com/ckeditor/ckeditor5-utils/compare/v0.7.0...v0.8.0) (2017-03-06) | ||
@@ -5,0 +18,0 @@ |
{ | ||
"name": "@ckeditor/ckeditor5-utils", | ||
"version": "0.8.0", | ||
"version": "0.9.0", | ||
"description": "CKEditor 5 Utils", | ||
@@ -10,4 +10,4 @@ "keywords": [ | ||
"@ckeditor/ckeditor5-dev-lint": "^2.0.2", | ||
"@ckeditor/ckeditor5-core": "^0.7.0", | ||
"@ckeditor/ckeditor5-engine": "^0.8.0", | ||
"@ckeditor/ckeditor5-core": "^0.8.0", | ||
"@ckeditor/ckeditor5-engine": "^0.9.0", | ||
"del": "^2.2.0", | ||
@@ -14,0 +14,0 @@ "gulp": "^3.9.0", |
@@ -31,3 +31,2 @@ /** | ||
* | ||
* @param {Iterable} [items] Items to be added to the collection. | ||
* @param {Object} options The options object. | ||
@@ -60,2 +59,34 @@ * @param {String} [options.idProperty='id'] The name of the property which is considered to identify an item. | ||
this._idProperty = options && options.idProperty || 'id'; | ||
/** | ||
* A helper mapping external items of a bound collection ({@link #bindTo}) | ||
* and actual items of this collection. It provides information | ||
* necessary to properly remove items bound to another collection. | ||
* | ||
* See {@link #_bindToInternalToExternalMap}. | ||
* | ||
* @protected | ||
* @member {WeakMap} | ||
*/ | ||
this._bindToExternalToInternalMap = new WeakMap(); | ||
/** | ||
* A helper mapping items of this collection to external items of a bound collection | ||
* ({@link #bindTo}). It provides information necessary to manage the bindings, e.g. | ||
* to avoid loops in two–way bindings. | ||
* | ||
* See {@link #_bindToExternalToInternalMap}. | ||
* | ||
* @protected | ||
* @member {WeakMap} | ||
*/ | ||
this._bindToInternalToExternalMap = new WeakMap(); | ||
/** | ||
* A collection instance this collection is bound to as a result | ||
* of calling {@link #bindTo} method. | ||
* | ||
* @protected | ||
* @member {module:utils/collection~Collection} #_bindToCollection | ||
*/ | ||
} | ||
@@ -223,2 +254,6 @@ | ||
const externalItem = this._bindToInternalToExternalMap.get( item ); | ||
this._bindToInternalToExternalMap.delete( item ); | ||
this._bindToExternalToInternalMap.delete( externalItem ); | ||
this.fire( 'remove', item ); | ||
@@ -269,5 +304,11 @@ | ||
/** | ||
* Removes all items from the collection. | ||
* Removes all items from the collection and destroys the binding created using | ||
* {@link #bindTo}. | ||
*/ | ||
clear() { | ||
if ( this._bindToCollection ) { | ||
this.stopListening( this._bindToCollection ); | ||
this._bindToCollection = null; | ||
} | ||
while ( this.length ) { | ||
@@ -279,2 +320,173 @@ this.remove( 0 ); | ||
/** | ||
* Binds and synchronizes the collection with another one. | ||
* | ||
* The binding can be a simple factory: | ||
* | ||
* class FactoryClass { | ||
* constructor( data ) { | ||
* this.label = data.label; | ||
* } | ||
* } | ||
* | ||
* const source = new Collection( { idProperty: 'label' } ); | ||
* const target = new Collection(); | ||
* | ||
* target.bindTo( source ).as( FactoryClass ); | ||
* | ||
* source.add( { label: 'foo' } ); | ||
* source.add( { label: 'bar' } ); | ||
* | ||
* console.log( target.length ); // 2 | ||
* console.log( target.get( 1 ).label ); // 'bar' | ||
* | ||
* source.remove( 0 ); | ||
* console.log( target.length ); // 1 | ||
* console.log( target.get( 0 ).label ); // 'bar' | ||
* | ||
* or the factory driven by a custom callback: | ||
* | ||
* class FooClass { | ||
* constructor( data ) { | ||
* this.label = data.label; | ||
* } | ||
* } | ||
* | ||
* class BarClass { | ||
* constructor( data ) { | ||
* this.label = data.label; | ||
* } | ||
* } | ||
* | ||
* const source = new Collection( { idProperty: 'label' } ); | ||
* const target = new Collection(); | ||
* | ||
* target.bindTo( source ).using( ( item ) => { | ||
* if ( item.label == 'foo' ) { | ||
* return new FooClass( item ); | ||
* } else { | ||
* return new BarClass( item ); | ||
* } | ||
* } ); | ||
* | ||
* source.add( { label: 'foo' } ); | ||
* source.add( { label: 'bar' } ); | ||
* | ||
* console.log( target.length ); // 2 | ||
* console.log( target.get( 0 ) instanceof FooClass ); // true | ||
* console.log( target.get( 1 ) instanceof BarClass ); // true | ||
* | ||
* or the factory out of property name: | ||
* | ||
* const source = new Collection( { idProperty: 'label' } ); | ||
* const target = new Collection(); | ||
* | ||
* target.bindTo( source ).using( 'label' ); | ||
* | ||
* source.add( { label: { value: 'foo' } } ); | ||
* source.add( { label: { value: 'bar' } } ); | ||
* | ||
* console.log( target.length ); // 2 | ||
* console.log( target.get( 0 ).value ); // 'foo' | ||
* console.log( target.get( 1 ).value ); // 'bar' | ||
* | ||
* **Note**: {@link #clear} can be used to break the binding. | ||
* | ||
* @param {module:utils/collection~Collection} collection A collection to be bound. | ||
* @returns {Object} | ||
* @returns {module:utils/collection~Collection#bindTo#as} return.as | ||
* @returns {module:utils/collection~Collection#bindTo#using} return.using | ||
*/ | ||
bindTo( externalCollection ) { | ||
if ( this._bindToCollection ) { | ||
/** | ||
* The collection cannot be bound more than once. | ||
* | ||
* @error collection-bind-to-rebind | ||
*/ | ||
throw new CKEditorError( 'collection-bind-to-rebind: The collection cannot be bound more than once.' ); | ||
} | ||
this._bindToCollection = externalCollection; | ||
return { | ||
/** | ||
* Creates the class factory binding. | ||
* | ||
* @static | ||
* @param {Function} Class Specifies which class factory is to be initialized. | ||
*/ | ||
as: ( Class ) => { | ||
this._setUpBindToBinding( item => new Class( item ) ); | ||
}, | ||
/** | ||
* Creates callback or property binding. | ||
* | ||
* @static | ||
* @param {Function|String} callbackOrProperty When the function is passed, it is used to | ||
* produce the items. When the string is provided, the property value is used to create | ||
* the bound collection items. | ||
*/ | ||
using: ( callbackOrProperty ) => { | ||
if ( typeof callbackOrProperty == 'function' ) { | ||
this._setUpBindToBinding( item => callbackOrProperty( item ) ); | ||
} else { | ||
this._setUpBindToBinding( item => item[ callbackOrProperty ] ); | ||
} | ||
} | ||
}; | ||
} | ||
/** | ||
* Finalizes and activates a binding initiated by {#bindTo}. | ||
* | ||
* @protected | ||
* @param {Function} factory A function which produces collection items. | ||
*/ | ||
_setUpBindToBinding( factory ) { | ||
const externalCollection = this._bindToCollection; | ||
// Adds the item to the collection once a change has been done to the external collection. | ||
// | ||
// @private | ||
const addItem = ( evt, externalItem, index ) => { | ||
const isExternalBoundToThis = externalCollection._bindToCollection == this; | ||
const externalItemBound = externalCollection._bindToInternalToExternalMap.get( externalItem ); | ||
// If an external collection is bound to this collection, which makes it a 2–way binding, | ||
// and the particular external collection item is already bound, don't add it here. | ||
// The external item has been created **out of this collection's item** and (re)adding it will | ||
// cause a loop. | ||
if ( isExternalBoundToThis && externalItemBound ) { | ||
this._bindToExternalToInternalMap.set( externalItem, externalItemBound ); | ||
this._bindToInternalToExternalMap.set( externalItemBound, externalItem ); | ||
} else { | ||
const item = factory( externalItem ); | ||
this._bindToExternalToInternalMap.set( externalItem, item ); | ||
this._bindToInternalToExternalMap.set( item, externalItem ); | ||
this.add( item, index ); | ||
} | ||
}; | ||
// Load the initial content of the collection. | ||
for ( let externalItem of externalCollection ) { | ||
addItem( null, externalItem ); | ||
} | ||
// Synchronize the with collection as new items are added. | ||
this.listenTo( externalCollection, 'add', addItem ); | ||
// Synchronize the with collection as new items are removed. | ||
this.listenTo( externalCollection, 'remove', ( evt, externalItem ) => { | ||
const item = this._bindToExternalToInternalMap.get( externalItem ); | ||
if ( item ) { | ||
this.remove( item ); | ||
} | ||
} ); | ||
} | ||
/** | ||
* Collection iterator. | ||
@@ -281,0 +493,0 @@ */ |
@@ -102,10 +102,28 @@ /** | ||
// (#126) If there's some positioned ancestor of the panel, then its rect must be taken into | ||
// consideration. `Rect` is always relative to the viewport while `position: absolute` works | ||
// with respect to that positioned ancestor. | ||
if ( positionedElementAncestor ) { | ||
const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) ); | ||
const ancestorComputedStyles = global.window.getComputedStyle( positionedElementAncestor ); | ||
// (https://github.com/ckeditor/ckeditor5-ui-default/issues/126) | ||
// If there's some positioned ancestor of the panel, then its `Rect` must be taken into | ||
// consideration. `Rect` is always relative to the viewport while `position: absolute` works | ||
// with respect to that positioned ancestor. | ||
left -= ancestorPosition.left; | ||
top -= ancestorPosition.top; | ||
// (https://github.com/ckeditor/ckeditor5-utils/issues/139) | ||
// If there's some positioned ancestor of the panel, not only its position must be taken into | ||
// consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect` | ||
// is relative to the viewport (it doesn't care about scrolling), while `position: absolute` | ||
// must compensate that scrolling. | ||
left += positionedElementAncestor.scrollLeft; | ||
top += positionedElementAncestor.scrollTop; | ||
// (https://github.com/ckeditor/ckeditor5-utils/issues/139) | ||
// If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth` | ||
// while `position: absolute` positioning does not consider it. | ||
// E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element, | ||
// not upper-left corner of its border. | ||
left -= parseInt( ancestorComputedStyles.borderLeftWidth, 10 ); | ||
top -= parseInt( ancestorComputedStyles.borderTopWidth, 10 ); | ||
} | ||
@@ -112,0 +130,0 @@ |
@@ -35,3 +35,3 @@ /** | ||
* | ||
* @private | ||
* @protected | ||
* @member {module:utils/dom/emittermixin~Emitter} | ||
@@ -38,0 +38,0 @@ */ |
@@ -11,4 +11,9 @@ /** | ||
/** | ||
* Returns `nth` (starts from `0` of course) item of an `iterable`. | ||
* Returns `nth` (starts from `0` of course) item of the given `iterable`. | ||
* | ||
* If the iterable is a generator, then it consumes **all its items**. | ||
* If it's a normal iterator, then it consumes **all items up to the given index**. | ||
* Refer to the [Iterators and Generators](https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Iterators_and_Generators) | ||
* guide to learn differences between these interfaces. | ||
* | ||
* @param {Number} index | ||
@@ -19,3 +24,3 @@ * @param {Iterable.<*>} iterable | ||
export default function nth( index, iterable ) { | ||
for ( let item of iterable ) { | ||
for ( const item of iterable ) { | ||
if ( index === 0 ) { | ||
@@ -22,0 +27,0 @@ return item; |
@@ -27,3 +27,3 @@ /** | ||
describe( 'constructor', () => { | ||
describe( 'constructor()', () => { | ||
it( 'allows to change the id property used by the collection', () => { | ||
@@ -44,3 +44,3 @@ let item1 = { id: 'foo', name: 'xx' }; | ||
describe( 'add', () => { | ||
describe( 'add()', () => { | ||
it( 'should be chainable', () => { | ||
@@ -211,3 +211,2 @@ expect( collection.add( {} ) ).to.equal( collection ); | ||
it( 'should support an optional index argument', () => { | ||
let collection = new Collection(); | ||
let item1 = getItem( 'foo' ); | ||
@@ -230,3 +229,2 @@ let item2 = getItem( 'bar' ); | ||
it( 'should throw when index argument is invalid', () => { | ||
let collection = new Collection(); | ||
let item1 = getItem( 'foo' ); | ||
@@ -267,3 +265,3 @@ let item2 = getItem( 'bar' ); | ||
describe( 'get', () => { | ||
describe( 'get()', () => { | ||
it( 'should return an item', () => { | ||
@@ -289,3 +287,3 @@ let item = getItem( 'foo' ); | ||
describe( 'getIndex', () => { | ||
describe( 'getIndex()', () => { | ||
it( 'should return index of given item', () => { | ||
@@ -326,3 +324,3 @@ const item1 = { foo: 'bar' }; | ||
describe( 'remove', () => { | ||
describe( 'remove()', () => { | ||
it( 'should remove the model by index', () => { | ||
@@ -454,3 +452,3 @@ collection.add( getItem( 'bom' ) ); | ||
describe( 'map', () => { | ||
describe( 'map()', () => { | ||
it( 'uses native map', () => { | ||
@@ -471,3 +469,3 @@ let spy = testUtils.sinon.stub( Array.prototype, 'map', () => { | ||
describe( 'find', () => { | ||
describe( 'find()', () => { | ||
it( 'uses native find', () => { | ||
@@ -490,3 +488,3 @@ let needl = getItem( 'foo' ); | ||
describe( 'filter', () => { | ||
describe( 'filter()', () => { | ||
it( 'uses native filter', () => { | ||
@@ -509,3 +507,3 @@ let needl = getItem( 'foo' ); | ||
describe( 'clear', () => { | ||
describe( 'clear()', () => { | ||
it( 'removes all items', () => { | ||
@@ -524,4 +522,492 @@ const items = [ {}, {}, {} ]; | ||
} ); | ||
it( 'breaks the binding', () => { | ||
const external = new Collection(); | ||
collection.bindTo( external ).using( i => i ); | ||
external.add( { foo: 'bar' } ); | ||
expect( collection ).to.have.length( 1 ); | ||
collection.clear(); | ||
external.add( { foo: 'baz' } ); | ||
expect( collection ).to.have.length( 0 ); | ||
external.remove( 0 ); | ||
expect( collection ).to.have.length( 0 ); | ||
expect( collection._bindToCollection ).to.be.null; | ||
} ); | ||
} ); | ||
describe( 'bindTo()', () => { | ||
class FactoryClass { | ||
constructor( data ) { | ||
this.data = data; | ||
} | ||
} | ||
function assertItems( collection, expectedItems ) { | ||
expect( collection.map( i => i.v ) ).to.deep.equal( expectedItems ); | ||
} | ||
it( 'throws when binding more than once', () => { | ||
collection.bindTo( {} ); | ||
expect( () => { | ||
collection.bindTo( {} ); | ||
} ).to.throw( CKEditorError, /^collection-bind-to-rebind/ ); | ||
} ); | ||
it( 'provides "using()" and "as()" interfaces', () => { | ||
const returned = collection.bindTo( {} ); | ||
expect( returned ).to.have.keys( 'using', 'as' ); | ||
expect( returned.using ).to.be.a( 'function' ); | ||
expect( returned.as ).to.be.a( 'function' ); | ||
} ); | ||
it( 'stores reference to bound collection', () => { | ||
const collectionB = new Collection(); | ||
expect( collection._bindToCollection ).to.be.undefined; | ||
expect( collectionB._bindToCollection ).to.be.undefined; | ||
collection.bindTo( collectionB ).as( FactoryClass ); | ||
expect( collection._bindToCollection ).to.equal( collectionB ); | ||
expect( collectionB._bindToCollection ).to.be.undefined; | ||
} ); | ||
describe( 'as()', () => { | ||
let items; | ||
beforeEach( () => { | ||
items = new Collection(); | ||
} ); | ||
it( 'does not chain', () => { | ||
const returned = collection.bindTo( new Collection() ).as( FactoryClass ); | ||
expect( returned ).to.be.undefined; | ||
} ); | ||
it( 'creates a binding (initial content)', () => { | ||
items.add( { id: '1' } ); | ||
items.add( { id: '2' } ); | ||
collection.bindTo( items ).as( FactoryClass ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) ); | ||
} ); | ||
it( 'creates a binding (new content)', () => { | ||
collection.bindTo( items ).as( FactoryClass ); | ||
expect( collection ).to.have.length( 0 ); | ||
items.add( { id: '1' } ); | ||
items.add( { id: '2' } ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) ); | ||
} ); | ||
it( 'creates a binding (item removal)', () => { | ||
collection.bindTo( items ).as( FactoryClass ); | ||
expect( collection ).to.have.length( 0 ); | ||
items.add( { id: '1' } ); | ||
items.add( { id: '2' } ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) ); | ||
items.remove( 1 ); | ||
expect( collection.get( 0 ).data ).to.equal( items.get( 0 ) ); | ||
items.remove( 0 ); | ||
expect( collection ).to.have.length( 0 ); | ||
} ); | ||
} ); | ||
describe( 'using()', () => { | ||
let items; | ||
beforeEach( () => { | ||
items = new Collection(); | ||
} ); | ||
it( 'does not chain', () => { | ||
const returned = collection.bindTo( new Collection() ).using( () => {} ); | ||
expect( returned ).to.be.undefined; | ||
} ); | ||
describe( 'callback', () => { | ||
it( 'creates a binding (arrow function)', () => { | ||
collection.bindTo( items ).using( ( item ) => { | ||
return new FactoryClass( item ); | ||
} ); | ||
expect( collection ).to.have.length( 0 ); | ||
items.add( { id: '1' } ); | ||
items.add( { id: '2' } ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ).data ).to.equal( items.get( 1 ) ); | ||
} ); | ||
// https://github.com/ckeditor/ckeditor5-ui/issues/113 | ||
it( 'creates a binding (normal function)', () => { | ||
collection.bindTo( items ).using( function( item ) { | ||
return new FactoryClass( item ); | ||
} ); | ||
items.add( { id: '1' } ); | ||
expect( collection ).to.have.length( 1 ); | ||
const view = collection.get( 0 ); | ||
// Wrong args will be passed to the callback if it's treated as the view constructor. | ||
expect( view ).to.be.instanceOf( FactoryClass ); | ||
expect( view.data ).to.equal( items.get( 0 ) ); | ||
} ); | ||
it( 'creates a 1:1 binding', () => { | ||
collection.bindTo( items ).using( item => item ); | ||
expect( collection ).to.have.length( 0 ); | ||
const item1 = { id: '100' }; | ||
const item2 = { id: '200' }; | ||
items.add( item1 ); | ||
items.add( item2 ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ) ).to.equal( item1 ); | ||
expect( collection.get( 1 ) ).to.equal( item2 ); | ||
} ); | ||
it( 'creates a conditional binding', () => { | ||
class CustomClass { | ||
constructor( data ) { | ||
this.data = data; | ||
} | ||
} | ||
collection.bindTo( items ).using( item => { | ||
if ( item.id == 'FactoryClass' ) { | ||
return new FactoryClass( item ); | ||
} else { | ||
return new CustomClass( item ); | ||
} | ||
} ); | ||
expect( collection ).to.have.length( 0 ); | ||
const item1 = { id: 'FactoryClass' }; | ||
const item2 = { id: 'CustomClass' }; | ||
items.add( item1 ); | ||
items.add( item2 ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ) ).to.be.instanceOf( FactoryClass ); | ||
expect( collection.get( 1 ) ).to.be.instanceOf( CustomClass ); | ||
} ); | ||
it( 'creates a binding to a property name', () => { | ||
collection.bindTo( items ).using( item => item.prop ); | ||
expect( collection ).to.have.length( 0 ); | ||
items.add( { prop: { value: 'foo' } } ); | ||
items.add( { prop: { value: 'bar' } } ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ).value ).to.equal( 'foo' ); | ||
expect( collection.get( 1 ).value ).to.equal( 'bar' ); | ||
} ); | ||
} ); | ||
describe( 'property name', () => { | ||
it( 'creates a binding', () => { | ||
collection.bindTo( items ).using( 'prop' ); | ||
expect( collection ).to.have.length( 0 ); | ||
items.add( { prop: { value: 'foo' } } ); | ||
items.add( { prop: { value: 'bar' } } ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ).value ).to.equal( 'foo' ); | ||
expect( collection.get( 1 ).value ).to.equal( 'bar' ); | ||
} ); | ||
it( 'creates a binding (item removal)', () => { | ||
collection.bindTo( items ).using( 'prop' ); | ||
expect( collection ).to.have.length( 0 ); | ||
items.add( { prop: { value: 'foo' } } ); | ||
items.add( { prop: { value: 'bar' } } ); | ||
expect( collection ).to.have.length( 2 ); | ||
expect( collection.get( 0 ).value ).to.equal( 'foo' ); | ||
expect( collection.get( 1 ).value ).to.equal( 'bar' ); | ||
items.remove( 1 ); | ||
expect( collection ).to.have.length( 1 ); | ||
expect( collection.get( 0 ).value ).to.equal( 'foo' ); | ||
items.remove( 0 ); | ||
expect( collection ).to.have.length( 0 ); | ||
} ); | ||
} ); | ||
} ); | ||
describe( 'two–way data binding', () => { | ||
it( 'works with custom factories (1)', () => { | ||
const collectionA = new Collection(); | ||
const collectionB = new Collection(); | ||
const spyA = sinon.spy(); | ||
const spyB = sinon.spy(); | ||
collectionA.on( 'add', spyA ); | ||
collectionB.on( 'add', spyB ); | ||
// A<--->B | ||
collectionA.bindTo( collectionB ).using( i => ( { v: i.v * 2 } ) ); | ||
collectionB.bindTo( collectionA ).using( i => ( { v: i.v / 2 } ) ); | ||
assertItems( collectionA, [] ); | ||
assertItems( collectionB, [] ); | ||
collectionA.add( { v: 4 } ); | ||
collectionA.add( { v: 6 } ); | ||
assertItems( collectionA, [ 4, 6 ] ); | ||
assertItems( collectionB, [ 2, 3 ] ); | ||
collectionB.add( { v: 4 } ); | ||
assertItems( collectionA, [ 4, 6, 8 ] ); | ||
assertItems( collectionB, [ 2, 3, 4 ] ); | ||
sinon.assert.callCount( spyA, 3 ); | ||
sinon.assert.callCount( spyB, 3 ); | ||
} ); | ||
it( 'works with custom factories (2)', () => { | ||
const collectionA = new Collection(); | ||
const collectionB = new Collection(); | ||
const spyA = sinon.spy(); | ||
const spyB = sinon.spy(); | ||
collectionA.on( 'add', spyA ); | ||
collectionB.on( 'add', spyB ); | ||
// A<--->B | ||
collectionA.bindTo( collectionB ).using( 'data' ); | ||
collectionB.bindTo( collectionA ).using( i => new FactoryClass( i ) ); | ||
collectionA.add( { v: 4 } ); | ||
collectionA.add( { v: 6 } ); | ||
expect( [ ...collectionB ].every( i => i instanceof FactoryClass ) ).to.be.true; | ||
expect( [ ...collectionB ].map( i => i.data ) ).to.deep.equal( [ ...collectionA ] ); | ||
expect( collectionB.map( i => i.data.v ) ).to.deep.equal( [ 4, 6 ] ); | ||
expect( collectionA.map( i => i.v ) ).to.deep.equal( [ 4, 6 ] ); | ||
collectionB.add( new FactoryClass( { v: 8 } ) ); | ||
expect( [ ...collectionB ].every( i => i instanceof FactoryClass ) ).to.be.true; | ||
expect( [ ...collectionB ].map( i => i.data ) ).to.deep.equal( [ ...collectionA ] ); | ||
expect( collectionB.map( i => i.data.v ) ).to.deep.equal( [ 4, 6, 8 ] ); | ||
expect( collectionA.map( i => i.v ) ).to.deep.equal( [ 4, 6, 8 ] ); | ||
} ); | ||
it( 'works with custom factories (custom index)', () => { | ||
const collectionA = new Collection(); | ||
const collectionB = new Collection(); | ||
const spyA = sinon.spy(); | ||
const spyB = sinon.spy(); | ||
collectionA.on( 'add', spyA ); | ||
collectionB.on( 'add', spyB ); | ||
// A<--->B | ||
collectionA.bindTo( collectionB ).using( i => ( { v: i.v * 2 } ) ); | ||
collectionB.bindTo( collectionA ).using( i => ( { v: i.v / 2 } ) ); | ||
assertItems( collectionA, [] ); | ||
assertItems( collectionB, [] ); | ||
collectionA.add( { v: 4 } ); | ||
collectionA.add( { v: 6 }, 0 ); | ||
assertItems( collectionA, [ 6, 4 ] ); | ||
assertItems( collectionB, [ 3, 2 ] ); | ||
collectionB.add( { v: 4 }, 1 ); | ||
assertItems( collectionA, [ 6, 8, 4 ] ); | ||
assertItems( collectionB, [ 3, 4, 2 ] ); | ||
sinon.assert.callCount( spyA, 3 ); | ||
sinon.assert.callCount( spyB, 3 ); | ||
} ); | ||
it( 'works with 1:1 binding', () => { | ||
const collectionA = new Collection(); | ||
const collectionB = new Collection(); | ||
const spyA = sinon.spy(); | ||
const spyB = sinon.spy(); | ||
collectionA.on( 'add', spyA ); | ||
collectionB.on( 'add', spyB ); | ||
// A<--->B | ||
collectionA.bindTo( collectionB ).using( i => i ); | ||
collectionB.bindTo( collectionA ).using( i => i ); | ||
assertItems( collectionA, [], [] ); | ||
assertItems( collectionB, [], [] ); | ||
collectionA.add( { v: 4 } ); | ||
collectionA.add( { v: 6 } ); | ||
assertItems( collectionA, [ 4, 6 ] ); | ||
assertItems( collectionB, [ 4, 6 ] ); | ||
collectionB.add( { v: 8 } ); | ||
assertItems( collectionA, [ 4, 6, 8 ] ); | ||
assertItems( collectionB, [ 4, 6, 8 ] ); | ||
expect( collectionA ).to.deep.equal( collectionB ); | ||
sinon.assert.callCount( spyA, 3 ); | ||
sinon.assert.callCount( spyB, 3 ); | ||
} ); | ||
it( 'works with double chaining', () => { | ||
const collectionA = new Collection(); | ||
const collectionB = new Collection(); | ||
const collectionC = new Collection(); | ||
const spyA = sinon.spy(); | ||
const spyB = sinon.spy(); | ||
const spyC = sinon.spy(); | ||
collectionA.on( 'add', spyA ); | ||
collectionB.on( 'add', spyB ); | ||
collectionC.on( 'add', spyC ); | ||
// A<--->B--->C | ||
collectionA.bindTo( collectionB ).using( i => ( { v: i.v * 2 } ) ); | ||
collectionB.bindTo( collectionA ).using( i => ( { v: i.v / 2 } ) ); | ||
collectionC.bindTo( collectionB ).using( i => ( { v: -i.v } ) ); | ||
assertItems( collectionA, [] ); | ||
assertItems( collectionB, [] ); | ||
assertItems( collectionC, [] ); | ||
collectionA.add( { v: 4 } ); | ||
collectionA.add( { v: 6 } ); | ||
assertItems( collectionA, [ 4, 6 ] ); | ||
assertItems( collectionB, [ 2, 3 ] ); | ||
assertItems( collectionC, [ -2, -3 ] ); | ||
collectionB.add( { v: 4 } ); | ||
assertItems( collectionA, [ 4, 6, 8 ] ); | ||
assertItems( collectionB, [ 2, 3, 4 ] ); | ||
assertItems( collectionC, [ -2, -3, -4 ] ); | ||
collectionC.add( { v: -1000 } ); | ||
assertItems( collectionA, [ 4, 6, 8 ] ); | ||
assertItems( collectionB, [ 2, 3, 4 ] ); | ||
assertItems( collectionC, [ -2, -3, -4, -1000 ] ); | ||
sinon.assert.callCount( spyA, 3 ); | ||
sinon.assert.callCount( spyB, 3 ); | ||
sinon.assert.callCount( spyC, 4 ); | ||
} ); | ||
it( 'removes items correctly', () => { | ||
const collectionA = new Collection(); | ||
const collectionB = new Collection(); | ||
const spyAddA = sinon.spy(); | ||
const spyAddB = sinon.spy(); | ||
const spyRemoveA = sinon.spy(); | ||
const spyRemoveB = sinon.spy(); | ||
collectionA.on( 'add', spyAddA ); | ||
collectionB.on( 'add', spyAddB ); | ||
collectionA.on( 'remove', spyRemoveA ); | ||
collectionB.on( 'remove', spyRemoveB ); | ||
// A<--->B | ||
collectionA.bindTo( collectionB ).using( i => ( { v: i.v * 2 } ) ); | ||
collectionB.bindTo( collectionA ).using( i => ( { v: i.v / 2 } ) ); | ||
assertItems( collectionA, [], [] ); | ||
assertItems( collectionB, [], [] ); | ||
collectionA.add( { v: 4 } ); | ||
collectionA.add( { v: 6 } ); | ||
assertItems( collectionA, [ 4, 6 ] ); | ||
assertItems( collectionB, [ 2, 3 ] ); | ||
collectionB.add( { v: 4 } ); | ||
assertItems( collectionA, [ 4, 6, 8 ] ); | ||
assertItems( collectionB, [ 2, 3, 4 ] ); | ||
collectionB.remove( 0 ); | ||
assertItems( collectionA, [ 6, 8 ] ); | ||
assertItems( collectionB, [ 3, 4 ] ); | ||
sinon.assert.callCount( spyAddA, 3 ); | ||
sinon.assert.callCount( spyAddB, 3 ); | ||
sinon.assert.callCount( spyRemoveA, 1 ); | ||
sinon.assert.callCount( spyRemoveB, 1 ); | ||
collectionA.remove( 1 ); | ||
assertItems( collectionA, [ 6 ] ); | ||
assertItems( collectionB, [ 3 ] ); | ||
sinon.assert.callCount( spyAddA, 3 ); | ||
sinon.assert.callCount( spyAddB, 3 ); | ||
sinon.assert.callCount( spyRemoveA, 2 ); | ||
sinon.assert.callCount( spyRemoveB, 2 ); | ||
} ); | ||
} ); | ||
} ); | ||
describe( 'iterator', () => { | ||
@@ -528,0 +1014,0 @@ it( 'covers the whole collection', () => { |
@@ -6,15 +6,10 @@ /** | ||
/* global document, window */ | ||
import global from '../../src/dom/global'; | ||
import { getOptimalPosition } from '../../src/dom/position'; | ||
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; | ||
testUtils.createSinonSandbox(); | ||
let element, target, limiter; | ||
let element, target, limiter, windowStub; | ||
describe( 'getOptimalPosition', () => { | ||
describe( 'getOptimalPosition()', () => { | ||
beforeEach( () => { | ||
windowStub = { | ||
stubWindow( { | ||
innerWidth: 10000, | ||
@@ -24,5 +19,3 @@ innerHeight: 10000, | ||
scrollY: 0 | ||
}; | ||
testUtils.sinon.stub( global, 'window', windowStub ); | ||
} ); | ||
} ); | ||
@@ -42,3 +35,3 @@ | ||
it( 'should return coordinates (window scroll)', () => { | ||
Object.assign( windowStub, { | ||
stubWindow( { | ||
innerWidth: 10000, | ||
@@ -57,37 +50,63 @@ innerHeight: 10000, | ||
it( 'should return coordinates (positioned element parent)', () => { | ||
const positionedParent = document.createElement( 'div' ); | ||
describe( 'positioned element parent', () => { | ||
let parent; | ||
Object.assign( windowStub, { | ||
innerWidth: 10000, | ||
innerHeight: 10000, | ||
scrollX: 1000, | ||
scrollY: 1000, | ||
getComputedStyle: ( el ) => { | ||
return window.getComputedStyle( el ); | ||
} | ||
} ); | ||
it( 'should return coordinates', () => { | ||
stubWindow( { | ||
innerWidth: 10000, | ||
innerHeight: 10000, | ||
scrollX: 1000, | ||
scrollY: 1000 | ||
} ); | ||
Object.assign( positionedParent.style, { | ||
position: 'absolute', | ||
top: '1000px', | ||
left: '1000px' | ||
} ); | ||
parent = getElement( { | ||
top: 1000, | ||
right: 1010, | ||
bottom: 1010, | ||
left: 1000, | ||
width: 10, | ||
height: 10 | ||
}, { | ||
position: 'absolute' | ||
} ); | ||
document.body.appendChild( positionedParent ); | ||
positionedParent.appendChild( element ); | ||
element.parentElement = parent; | ||
stubElementRect( positionedParent, { | ||
top: 1000, | ||
right: 1010, | ||
bottom: 1010, | ||
left: 1000, | ||
width: 10, | ||
height: 10 | ||
assertPosition( { element, target, positions: [ attachLeft ] }, { | ||
top: -900, | ||
left: -920, | ||
name: 'left' | ||
} ); | ||
} ); | ||
assertPosition( { element, target, positions: [ attachLeft ] }, { | ||
top: -900, | ||
left: -920, | ||
name: 'left' | ||
it( 'should return coordinates (scroll and border)', () => { | ||
stubWindow( { | ||
innerWidth: 10000, | ||
innerHeight: 10000, | ||
scrollX: 1000, | ||
scrollY: 1000 | ||
} ); | ||
parent = getElement( { | ||
top: 0, | ||
right: 10, | ||
bottom: 10, | ||
left: 0, | ||
width: 10, | ||
height: 10, | ||
scrollTop: 100, | ||
scrollLeft: 200 | ||
}, { | ||
position: 'absolute', | ||
borderLeftWidth: '20px', | ||
borderTopWidth: '40px', | ||
} ); | ||
element.parentElement = parent; | ||
assertPosition( { element, target, positions: [ attachLeft ] }, { | ||
top: 160, | ||
left: 260, | ||
name: 'left' | ||
} ); | ||
} ); | ||
@@ -259,3 +278,3 @@ } ); | ||
it( 'should return the very first coordinates if limiter does not fit into the viewport', () => { | ||
stubElementRect( limiter, { | ||
limiter = getElement( { | ||
top: -100, | ||
@@ -328,10 +347,39 @@ right: -80, | ||
function stubElementRect( element, rect ) { | ||
if ( element.getBoundingClientRect.restore ) { | ||
element.getBoundingClientRect.restore(); | ||
// Returns a synthetic element. | ||
// | ||
// @private | ||
// @param {Object} properties A set of properties for the element. | ||
// @param {Object} styles A set of styles in `window.getComputedStyle()` format. | ||
function getElement( properties = {}, styles = {} ) { | ||
const element = { | ||
tagName: 'div', | ||
scrollLeft: 0, | ||
scrollTop: 0 | ||
}; | ||
Object.assign( element, properties ); | ||
if ( !styles.borderLeftWidth ) { | ||
styles.borderLeftWidth = '0px'; | ||
} | ||
testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( rect ); | ||
if ( !styles.borderTopWidth ) { | ||
styles.borderTopWidth = '0px'; | ||
} | ||
global.window.getComputedStyle.withArgs( element ).returns( styles ); | ||
return element; | ||
} | ||
// Stubs the window. | ||
// | ||
// @private | ||
// @param {Object} properties A set of properties the window should have. | ||
function stubWindow( properties ) { | ||
global.window = Object.assign( { | ||
getComputedStyle: sinon.stub() | ||
}, properties ); | ||
} | ||
// <-- 100px -> | ||
@@ -352,6 +400,3 @@ // | ||
function setElementTargetPlayground() { | ||
element = document.createElement( 'div' ); | ||
target = document.createElement( 'div' ); | ||
stubElementRect( element, { | ||
element = getElement( { | ||
top: 0, | ||
@@ -365,3 +410,3 @@ right: 20, | ||
stubElementRect( target, { | ||
target = getElement( { | ||
top: 100, | ||
@@ -398,7 +443,3 @@ right: 110, | ||
function setElementTargetLimiterPlayground() { | ||
element = document.createElement( 'div' ); | ||
target = document.createElement( 'div' ); | ||
limiter = document.createElement( 'div' ); | ||
stubElementRect( element, { | ||
element = getElement( { | ||
top: 0, | ||
@@ -412,3 +453,3 @@ right: 20, | ||
stubElementRect( limiter, { | ||
limiter = getElement( { | ||
top: 100, | ||
@@ -422,3 +463,3 @@ right: 10, | ||
stubElementRect( target, { | ||
target = getElement( { | ||
top: 100, | ||
@@ -425,0 +466,0 @@ right: 10, |
@@ -11,15 +11,15 @@ /** | ||
it( 'should return 0th item', () => { | ||
expect( nth( 0, getIterator() ) ).to.equal( 11 ); | ||
expect( nth( 0, getGenerator() ) ).to.equal( 11 ); | ||
} ); | ||
it( 'should return the last item', () => { | ||
expect( nth( 2, getIterator() ) ).to.equal( 33 ); | ||
expect( nth( 2, getGenerator() ) ).to.equal( 33 ); | ||
} ); | ||
it( 'should return null if out of range (bottom)', () => { | ||
expect( nth( -1, getIterator() ) ).to.be.null; | ||
expect( nth( -1, getGenerator() ) ).to.be.null; | ||
} ); | ||
it( 'should return null if out of range (top)', () => { | ||
expect( nth( 3, getIterator() ) ).to.be.null; | ||
expect( nth( 3, getGenerator() ) ).to.be.null; | ||
} ); | ||
@@ -31,3 +31,20 @@ | ||
function *getIterator() { | ||
it( 'should consume the given generator', () => { | ||
const generator = getGenerator(); | ||
nth( 0, generator ); | ||
expect( generator.next().done ).to.equal( true ); | ||
} ); | ||
it( 'should stop inside the given iterator', () => { | ||
const collection = [ 11, 22, 33 ]; | ||
const iterator = collection[ Symbol.iterator ](); | ||
nth( 0, iterator ); | ||
expect( iterator.next().value ).to.equal( 22 ); | ||
} ); | ||
function *getGenerator() { | ||
yield 11; | ||
@@ -34,0 +51,0 @@ yield 22; |
Sorry, the diff of this file is not supported yet
921489
698
28094