This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #214 from ckeditor/t/213
Feature: Introduced `ImageLoadObserver`. Closes #213.
- Loading branch information
Showing
4 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/** | ||
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/** | ||
* @module image/image/imageloadobserver | ||
*/ | ||
|
||
import Observer from '@ckeditor/ckeditor5-engine/src/view/observer/observer'; | ||
|
||
/** | ||
* Observes all new images added to the {@link module:engine/view/document~Document}, | ||
* fires {@link module:engine/view/document~Document#event:imageLoaded} and | ||
* {@link module:engine/view/document~Document#layoutChanged} event every time when the new image | ||
* has been loaded. | ||
* | ||
* **Note:** This event is not fired for images that has been added to the document and rendered as `complete` (already loaded). | ||
* | ||
* @extends module:engine/view/observer/observer~Observer | ||
*/ | ||
export default class ImageLoadObserver extends Observer { | ||
constructor( view ) { | ||
super( view ); | ||
|
||
/** | ||
* List of img DOM elements that are observed by this observer. | ||
* | ||
* @private | ||
* @type {Set.<HTMLElement>} | ||
*/ | ||
this._observedElements = new Set(); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
observe( domRoot, name ) { | ||
const viewRoot = this.document.getRoot( name ); | ||
|
||
// When there is a change in one of the view element | ||
// we need to check if there are any new `<img/>` elements to observe. | ||
viewRoot.on( 'change:children', ( evt, node ) => { | ||
// Wait for the render to be sure that `<img/>` elements are rendered in the DOM root. | ||
this.view.once( 'render', () => this._updateObservedElements( domRoot, node ) ); | ||
} ); | ||
} | ||
|
||
/** | ||
* Updates the list of observed `<img/>` elements. | ||
* | ||
* @private | ||
* @param {HTMLElement} domRoot DOM root element. | ||
* @param {module:engine/view/element~Element} viewNode View element where children have changed. | ||
*/ | ||
_updateObservedElements( domRoot, viewNode ) { | ||
if ( !viewNode.is( 'element' ) || viewNode.is( 'attributeElement' ) ) { | ||
return; | ||
} | ||
|
||
const domNode = this.view.domConverter.mapViewToDom( viewNode ); | ||
|
||
// If there is no `domNode` it means that it was removed from the DOM in the meanwhile. | ||
if ( !domNode ) { | ||
return; | ||
} | ||
|
||
for ( const domElement of domNode.querySelectorAll( 'img' ) ) { | ||
if ( !this._observedElements.has( domElement ) ) { | ||
this.listenTo( domElement, 'load', ( evt, domEvt ) => this._fireEvents( domEvt ) ); | ||
this._observedElements.add( domElement ); | ||
} | ||
} | ||
|
||
// Clean up the list of observed elements from elements that has been removed from the root. | ||
for ( const domElement of this._observedElements ) { | ||
if ( !domRoot.contains( domElement ) ) { | ||
this.stopListening( domElement ); | ||
this._observedElements.delete( domElement ); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Fires {@link module:engine/view/view/document~Document#event:layoutChanged} and | ||
* {@link module:engine/view/document~Document#event:imageLoaded} | ||
* if observer {@link #isEnabled is enabled}. | ||
* | ||
* @protected | ||
* @param {Event} domEvent The DOM event. | ||
*/ | ||
_fireEvents( domEvent ) { | ||
if ( this.isEnabled ) { | ||
this.document.fire( 'layoutChanged' ); | ||
this.document.fire( 'imageLoaded', domEvent ); | ||
} | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
destroy() { | ||
this._observedElements.clear(); | ||
super.destroy(); | ||
} | ||
} | ||
|
||
/** | ||
* Fired when an <img/> DOM element has been loaded in the DOM root. | ||
* | ||
* Introduced by {@link module:image/image/imageloadobserver~ImageLoadObserver}. | ||
* | ||
* @see image/image/imageloadobserver~ImageLoadObserver | ||
* @event module:engine/view/document~Document#event:imageLoaded | ||
* @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
/** | ||
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/* globals document, Event */ | ||
|
||
import ImageLoadObserver from '../../src/image/imageloadobserver'; | ||
import Observer from '@ckeditor/ckeditor5-engine/src/view/observer/observer'; | ||
import View from '@ckeditor/ckeditor5-engine/src/view/view'; | ||
import Position from '@ckeditor/ckeditor5-engine/src/view/position'; | ||
import Range from '@ckeditor/ckeditor5-engine/src/view/range'; | ||
import createViewRoot from '@ckeditor/ckeditor5-engine/tests/view/_utils/createroot'; | ||
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; | ||
|
||
describe( 'ImageLoadObserver', () => { | ||
let view, viewDocument, observer, domRoot, viewRoot; | ||
|
||
beforeEach( () => { | ||
view = new View(); | ||
viewDocument = view.document; | ||
observer = view.addObserver( ImageLoadObserver ); | ||
|
||
viewRoot = createViewRoot( viewDocument ); | ||
domRoot = document.createElement( 'div' ); | ||
view.attachDomRoot( domRoot ); | ||
} ); | ||
|
||
afterEach( () => { | ||
view.destroy(); | ||
} ); | ||
|
||
it( 'should extend Observer', () => { | ||
expect( observer ).instanceof( Observer ); | ||
} ); | ||
|
||
it( 'should fire `loadImage` event for images in the document that are loaded with a delay', () => { | ||
const spy = sinon.spy(); | ||
|
||
viewDocument.on( 'imageLoaded', spy ); | ||
|
||
setData( view, '<img src="foo.png" />' ); | ||
|
||
sinon.assert.notCalled( spy ); | ||
|
||
domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); | ||
|
||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
|
||
it( 'should fire `layoutChanged` along with `imageLoaded` event', () => { | ||
const layoutChangedSpy = sinon.spy(); | ||
const imageLoadedSpy = sinon.spy(); | ||
|
||
view.document.on( 'layoutChanged', layoutChangedSpy ); | ||
view.document.on( 'imageLoaded', imageLoadedSpy ); | ||
|
||
observer._fireEvents( {} ); | ||
|
||
sinon.assert.calledOnce( layoutChangedSpy ); | ||
sinon.assert.calledOnce( imageLoadedSpy ); | ||
} ); | ||
|
||
it( 'should not fire events when observer is disabled', () => { | ||
const layoutChangedSpy = sinon.spy(); | ||
const imageLoadedSpy = sinon.spy(); | ||
|
||
view.document.on( 'layoutChanged', layoutChangedSpy ); | ||
view.document.on( 'imageLoaded', imageLoadedSpy ); | ||
|
||
observer.isEnabled = false; | ||
|
||
observer._fireEvents( {} ); | ||
|
||
sinon.assert.notCalled( layoutChangedSpy ); | ||
sinon.assert.notCalled( imageLoadedSpy ); | ||
} ); | ||
|
||
it( 'should not fire `loadImage` event for images removed from document', () => { | ||
const spy = sinon.spy(); | ||
|
||
viewDocument.on( 'imageLoaded', spy ); | ||
|
||
setData( view, '<img src="foo.png" />' ); | ||
|
||
sinon.assert.notCalled( spy ); | ||
|
||
const img = domRoot.querySelector( 'img' ); | ||
|
||
setData( view, '' ); | ||
|
||
img.dispatchEvent( new Event( 'load' ) ); | ||
|
||
sinon.assert.notCalled( spy ); | ||
} ); | ||
|
||
it( 'should do nothing with an image when changes are in the other parent', () => { | ||
setData( view, '<container:p><attribute:b>foo</attribute:b></container:p><container:div><img src="foo.png" /></container:div>' ); | ||
|
||
const viewP = viewRoot.getChild( 0 ); | ||
const viewDiv = viewRoot.getChild( 1 ); | ||
|
||
const mapSpy = sinon.spy( view.domConverter, 'mapViewToDom' ); | ||
|
||
// Change only the paragraph. | ||
view.change( writer => { | ||
const text = writer.createText( 'foo', { b: true } ); | ||
|
||
writer.insert( Position.createAt( viewRoot.getChild( 0 ).getChild( 0 ) ), text ); | ||
writer.wrap( Range.createOn( text ), writer.createAttributeElement( 'b' ) ); | ||
} ); | ||
|
||
sinon.assert.calledWith( mapSpy, viewP ); | ||
sinon.assert.neverCalledWith( mapSpy, viewDiv ); | ||
} ); | ||
|
||
it( 'should not throw when synced child was removed in the meanwhile', () => { | ||
let viewDiv; | ||
|
||
const mapSpy = sinon.spy( view.domConverter, 'mapViewToDom' ); | ||
|
||
view.change( writer => { | ||
viewDiv = writer.createContainerElement( 'div' ); | ||
viewRoot.fire( 'change:children', viewDiv ); | ||
} ); | ||
|
||
expect( () => { | ||
view._renderer.render(); | ||
sinon.assert.calledWith( mapSpy, viewDiv ); | ||
} ).to.not.throw(); | ||
} ); | ||
|
||
it( 'should stop observing images on destroy', () => { | ||
const spy = sinon.spy(); | ||
|
||
viewDocument.on( 'imageLoaded', spy ); | ||
|
||
setData( view, '<img src="foo.png" />' ); | ||
|
||
observer.destroy(); | ||
|
||
domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); | ||
|
||
sinon.assert.notCalled( spy ); | ||
} ); | ||
} ); |