@ckeditor/ckeditor5-image
Advanced tools
Comparing version 0.2.0 to 0.3.0
{ | ||
"image widget": "Label for the image widget.", | ||
"Side image": "Label for the Side image option.", | ||
"Full size image": "Label for the Full size image option." | ||
} | ||
"Full size image": "Label for the Full size image option.", | ||
"Change alternate text": "Label for the Change alternate text button.", | ||
"Alternate image text": "Label for the Alternate image text option." | ||
} |
{ | ||
"name": "@ckeditor/ckeditor5-image", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Image feature for CKEditor 5.", | ||
@@ -9,3 +9,5 @@ "keywords": [], | ||
"@ckeditor/ckeditor5-engine": "*", | ||
"@ckeditor/ckeditor5-ui": "*" | ||
"@ckeditor/ckeditor5-ui": "*", | ||
"@ckeditor/ckeditor5-utils": "*", | ||
"@ckeditor/ckeditor5-theme-lark": "*" | ||
}, | ||
@@ -21,3 +23,2 @@ "devDependencies": { | ||
"@ckeditor/ckeditor5-undo": "*", | ||
"@ckeditor/ckeditor5-utils": "*", | ||
"gulp": "^3.9.1", | ||
@@ -24,0 +25,0 @@ "guppy-pre-commit": "^0.4.0" |
@@ -10,4 +10,2 @@ /** | ||
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; | ||
import ViewEmptyElement from '@ckeditor/ckeditor5-engine/src/view/emptyelement'; | ||
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; | ||
@@ -97,19 +95,34 @@ import { isImageWidget } from './utils'; | ||
/** | ||
* Converts model `image` element to view representation: | ||
* Creates image attribute converter for provided model conversion dispatchers. | ||
* | ||
* <figure class="image"><img src="..." alt="..."></img></figure> | ||
* | ||
* @param {module:engine/model/element~Element} modelElement | ||
* @return {module:engine/view/containerelement~ContainerElement} | ||
* @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers | ||
* @param {String} attributeName | ||
*/ | ||
export function modelToViewImage( modelElement ) { | ||
const viewImg = new ViewEmptyElement( 'img', { | ||
src: modelElement.getAttribute( 'src' ) | ||
} ); | ||
export function createImageAttributeConverter( dispatchers, attributeName ) { | ||
for ( let dispatcher of dispatchers ) { | ||
dispatcher.on( `addAttribute:${ attributeName }:image`, modelToViewAttributeConverter ); | ||
dispatcher.on( `changeAttribute:${ attributeName }:image`, modelToViewAttributeConverter ); | ||
dispatcher.on( `removeAttribute:${ attributeName }:image`, modelToViewAttributeConverter ); | ||
} | ||
} | ||
if ( modelElement.hasAttribute( 'alt' ) ) { | ||
viewImg.setAttribute( 'alt', modelElement.getAttribute( 'alt' ) ); | ||
// Model to view image converter converting given attribute, and adding it to `img` element nested inside `figure` element. | ||
// | ||
// @private | ||
function modelToViewAttributeConverter( evt, data, consumable, conversionApi ) { | ||
const parts = evt.name.split( ':' ); | ||
const consumableType = parts[ 0 ] + ':' + parts[ 1 ]; | ||
if ( !consumable.consume( data.item, consumableType ) ) { | ||
return; | ||
} | ||
return new ViewContainerElement( 'figure', { class: 'image' }, viewImg ); | ||
const figure = conversionApi.mapper.toViewElement( data.item ); | ||
const img = figure.getChild( 0 ); | ||
if ( parts[ 0 ] == 'removeAttribute' ) { | ||
img.removeAttribute( data.attributeKey ); | ||
} else { | ||
img.setAttribute( data.attributeKey, data.attributeNewValue ); | ||
} | ||
} |
@@ -13,2 +13,3 @@ /** | ||
import Widget from './widget/widget'; | ||
import ImageAlternateText from './imagealternatetext/imagealternatetext'; | ||
@@ -29,4 +30,4 @@ import '../theme/theme.scss'; | ||
static get requires() { | ||
return [ ImageEngine, Widget ]; | ||
return [ ImageEngine, Widget, ImageAlternateText ]; | ||
} | ||
} |
@@ -13,4 +13,6 @@ /** | ||
import WidgetEngine from './widget/widgetengine'; | ||
import { modelToViewImage, viewToModelImage, modelToViewSelection } from './converters'; | ||
import { viewToModelImage, modelToViewSelection, createImageAttributeConverter } from './converters'; | ||
import { toImageWidget } from './utils'; | ||
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; | ||
import ViewEmptyElement from '@ckeditor/ckeditor5-engine/src/view/emptyelement'; | ||
@@ -22,3 +24,3 @@ /** | ||
* | ||
* @extends module:core/plugin~Plugin. | ||
* @extends module:core/plugin~Plugin | ||
*/ | ||
@@ -52,3 +54,3 @@ export default class ImageEngine extends Plugin { | ||
.fromElement( 'image' ) | ||
.toElement( ( data ) => modelToViewImage( data.item ) ); | ||
.toElement( () => createImageViewElement() ); | ||
@@ -58,4 +60,7 @@ // Build converter from model to view for editing pipeline. | ||
.fromElement( 'image' ) | ||
.toElement( ( data ) => toImageWidget( modelToViewImage( data.item ) ) ); | ||
.toElement( () => toImageWidget( createImageViewElement() ) ); | ||
createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'src' ); | ||
createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'alt' ); | ||
// Converter for figure element from view to model. | ||
@@ -68,1 +73,13 @@ data.viewToModel.on( 'element:figure', viewToModelImage() ); | ||
} | ||
// Creates view element representing the image. | ||
// | ||
// <figure class="image"><img></img></figure> | ||
// | ||
// Note that `alt` and `src` attributes are converted separately, so they're not included. | ||
// | ||
// @private | ||
// @return {module:engine/view/containerelement~ContainerElement} | ||
export function createImageViewElement() { | ||
return new ViewContainerElement( 'figure', { class: 'image' }, new ViewEmptyElement( 'img' ) ); | ||
} |
@@ -10,35 +10,8 @@ /** | ||
import Template from '@ckeditor/ckeditor5-ui/src/template'; | ||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; | ||
import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; | ||
import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/balloonpanel/balloonpanelview'; | ||
import Template from '@ckeditor/ckeditor5-ui/src/template'; | ||
import { isImageWidget } from './utils'; | ||
import throttle from '@ckeditor/ckeditor5-utils/src/lib/lodash/throttle'; | ||
import global from '@ckeditor/ckeditor5-utils/src/dom/global'; | ||
import ImageBalloonPanel from './ui/imageballoonpanelview'; | ||
const arrowVOffset = BalloonPanelView.arrowVerticalOffset; | ||
const positions = { | ||
// [text range] | ||
// ^ | ||
// +-----------------+ | ||
// | Balloon | | ||
// +-----------------+ | ||
south: ( targetRect, balloonRect ) => ( { | ||
top: targetRect.bottom + arrowVOffset, | ||
left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, | ||
name: 's' | ||
} ), | ||
// +-----------------+ | ||
// | Balloon | | ||
// +-----------------+ | ||
// V | ||
// [text range] | ||
north: ( targetRect, balloonRect ) => ( { | ||
top: targetRect.top - balloonRect.height - arrowVOffset, | ||
left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, | ||
name: 'n' | ||
} ) | ||
}; | ||
/** | ||
@@ -61,2 +34,10 @@ * Image toolbar class. Creates image toolbar placed inside balloon panel that is showed when image widget is selected. | ||
editor.config.set( 'image.defaultToolbar', [] ); | ||
/** | ||
* When set to `true`, toolbar will be repositioned and showed on each render event and focus change. | ||
* Set to `false` to temporary disable the image toolbar. | ||
* | ||
* @member {Boolean} | ||
*/ | ||
this.isEnabled = true; | ||
} | ||
@@ -76,8 +57,7 @@ | ||
// Create a plain toolbar instance. | ||
const panel = this._panel = new ImageBalloonPanel( editor ); | ||
const promises = []; | ||
const toolbar = new ToolbarView(); | ||
// Create a BalloonPanelView instance. | ||
const panel = new BalloonPanelView( editor.locale ); | ||
// Add CSS class to the panel. | ||
Template.extend( panel.template, { | ||
@@ -91,52 +71,47 @@ attributes: { | ||
// Putting the toolbar inside of the balloon panel. | ||
panel.content.add( toolbar ); | ||
// Add toolbar to balloon panel. | ||
promises.push( panel.content.add( toolbar ) ); | ||
return editor.ui.view.body.add( panel ).then( () => { | ||
const editingView = editor.editing.view; | ||
const promises = []; | ||
// Add buttons to the toolbar. | ||
for ( let name of toolbarConfig ) { | ||
promises.push( toolbar.items.add( editor.ui.componentFactory.create( name ) ) ); | ||
} | ||
for ( let name of toolbarConfig ) { | ||
promises.push( toolbar.items.add( editor.ui.componentFactory.create( name ) ) ); | ||
// Add balloon panel to editor's UI. | ||
promises.push( editor.ui.view.body.add( panel ) ); | ||
// Show balloon panel each time image widget is selected. | ||
this.listenTo( this.editor.editing.view, 'render', () => { | ||
if ( this.isEnabled ) { | ||
this.show(); | ||
} | ||
}, { priority: 'low' } ); | ||
// Let the focusTracker know about new focusable UI element. | ||
editor.ui.focusTracker.add( panel.element ); | ||
// There is no render method after focus is back in editor, we need to check if balloon panel should be visible. | ||
this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is, was ) => { | ||
if ( !was && is && this.isEnabled ) { | ||
this.show(); | ||
} | ||
} ); | ||
// Hide the panel when editor loses focus but no the other way around. | ||
panel.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is, was ) => { | ||
if ( was && !is ) { | ||
panel.hide(); | ||
} | ||
} ); | ||
return Promise.all( promises ); | ||
} | ||
const attachToolbarCallback = throttle( attachToolbar, 100 ); | ||
/** | ||
* Shows the toolbar. | ||
*/ | ||
show() { | ||
const selectedElement = this.editor.editing.view.selection.getSelectedElement(); | ||
// Check if the toolbar should be displayed each time view is rendered. | ||
editor.listenTo( editingView, 'render', () => { | ||
const selectedElement = editingView.selection.getSelectedElement(); | ||
if ( selectedElement && isImageWidget( selectedElement ) ) { | ||
this._panel.attach(); | ||
} | ||
} | ||
if ( selectedElement && isImageWidget( selectedElement ) ) { | ||
attachToolbar(); | ||
editor.ui.view.listenTo( global.window, 'scroll', attachToolbarCallback ); | ||
editor.ui.view.listenTo( global.window, 'resize', attachToolbarCallback ); | ||
} else { | ||
panel.hide(); | ||
editor.ui.view.stopListening( global.window, 'scroll', attachToolbarCallback ); | ||
editor.ui.view.stopListening( global.window, 'resize', attachToolbarCallback ); | ||
} | ||
}, { priority: 'low' } ); | ||
function attachToolbar() { | ||
panel.attachTo( { | ||
target: editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ), | ||
positions: [ positions.north, positions.south ] | ||
} ); | ||
} | ||
return Promise.all( promises ); | ||
} ); | ||
/** | ||
* Hides the toolbar. | ||
*/ | ||
hide() { | ||
this._panel.detach(); | ||
} | ||
} |
@@ -12,2 +12,3 @@ /** | ||
import Widget from '../src/widget/widget'; | ||
import ImageAlternateText from '../src/imagealternatetext/imagealternatetext'; | ||
@@ -33,9 +34,13 @@ describe( 'Image', () => { | ||
it( 'should load ImageEngine feature', () => { | ||
it( 'should load ImageEngine plugin', () => { | ||
expect( editor.plugins.get( ImageEngine ) ).to.instanceOf( ImageEngine ); | ||
} ); | ||
it( 'should load Widget feature', () => { | ||
it( 'should load Widget plugin', () => { | ||
expect( editor.plugins.get( Widget ) ).to.instanceOf( Widget ); | ||
} ); | ||
it( 'should load ImageAlternateText plugin', () => { | ||
expect( editor.plugins.get( ImageAlternateText ) ).to.instanceOf( ImageAlternateText ); | ||
} ); | ||
} ); |
@@ -43,3 +43,3 @@ /** | ||
expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png" alt="alt text"></figure>' ); | ||
expect( editor.getData() ).to.equal( '<figure class="image"><img alt="alt text" src="foo.png"></figure>' ); | ||
} ); | ||
@@ -155,2 +155,48 @@ | ||
} ); | ||
it( 'should convert attribute change', () => { | ||
setModelData( document, '<image src="foo.png" alt="alt text"></image>' ); | ||
const image = document.getRoot().getChild( 0 ); | ||
document.enqueueChanges( () => { | ||
const batch = document.batch(); | ||
batch.setAttribute( image, 'alt', 'new text' ); | ||
} ); | ||
expect( getViewData( viewDocument, { withoutSelection: true } ) ) | ||
.to.equal( '<figure class="image ck-widget" contenteditable="false"><img alt="new text" src="foo.png"></img></figure>' ); | ||
} ); | ||
it( 'should convert attribute removal', () => { | ||
setModelData( document, '<image src="foo.png" alt="alt text"></image>' ); | ||
const image = document.getRoot().getChild( 0 ); | ||
document.enqueueChanges( () => { | ||
const batch = document.batch(); | ||
batch.removeAttribute( image, 'alt' ); | ||
} ); | ||
expect( getViewData( viewDocument, { withoutSelection: true } ) ) | ||
.to.equal( '<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>' ); | ||
} ); | ||
it( 'should not convert change if is already consumed', () => { | ||
setModelData( document, '<image src="foo.png" alt="alt text"></image>' ); | ||
const image = document.getRoot().getChild( 0 ); | ||
editor.editing.modelToView.on( 'removeAttribute:alt:image', ( evt, data, consumable ) => { | ||
consumable.consume( data.item, 'removeAttribute:alt' ); | ||
}, { priority: 'high' } ); | ||
document.enqueueChanges( () => { | ||
const batch = document.batch(); | ||
batch.removeAttribute( image, 'alt' ); | ||
} ); | ||
expect( getViewData( viewDocument, { withoutSelection: true } ) ) | ||
.to.equal( '<figure class="image ck-widget" contenteditable="false"><img alt="alt text" src="foo.png"></img></figure>' ); | ||
} ); | ||
} ); | ||
@@ -157,0 +203,0 @@ } ); |
@@ -6,4 +6,2 @@ /** | ||
/* global Event */ | ||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic'; | ||
@@ -13,3 +11,3 @@ import ImageToolbar from '../src/imagetoolbar'; | ||
import global from '@ckeditor/ckeditor5-utils/src/dom/global'; | ||
import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/balloonpanel/balloonpanelview'; | ||
import ImageBalloonPanel from '../src/ui/imageballoonpanelview'; | ||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; | ||
@@ -20,3 +18,3 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; | ||
describe( 'ImageToolbar', () => { | ||
let editor, button, editingView, doc, panel; | ||
let editor, button, editingView, doc, panel, plugin; | ||
@@ -37,3 +35,4 @@ beforeEach( () => { | ||
doc = editor.document; | ||
panel = getBalloonPanelView( editor.ui.view.body ); | ||
plugin = editor.plugins.get( ImageToolbar ); | ||
panel = plugin._panel; | ||
} ); | ||
@@ -51,3 +50,13 @@ } ); | ||
it( 'should initialize image.defaultToolbar to an empty array', () => { | ||
expect( editor.config.get( 'image.defaultToolbar' ) ).to.eql( [] ); | ||
const editorElement = global.document.createElement( 'div' ); | ||
global.document.body.appendChild( editorElement ); | ||
return ClassicEditor.create( editorElement, { | ||
plugins: [ ImageToolbar ], | ||
} ) | ||
.then( editor => { | ||
expect( editor.config.get( 'image.defaultToolbar' ) ).to.eql( [] ); | ||
return editor.destroy(); | ||
} ); | ||
} ); | ||
@@ -63,4 +72,3 @@ | ||
.then( newEditor => { | ||
const viewBody = newEditor.ui.view.body; | ||
expect( getBalloonPanelView( viewBody ) ).to.be.undefined; | ||
expect( newEditor.plugins.get( ImageToolbar )._panel ).to.be.undefined; | ||
@@ -79,3 +87,3 @@ newEditor.destroy(); | ||
.then( newEditor => { | ||
const panel = getBalloonPanelView( newEditor.ui.view.body ); | ||
const panel = newEditor.plugins.get( ImageToolbar )._panel; | ||
const toolbar = panel.content.get( 0 ); | ||
@@ -91,54 +99,38 @@ const button = toolbar.items.get( 0 ); | ||
it( 'should add BalloonPanelView to view body', () => { | ||
expect( panel ).to.be.instanceOf( BalloonPanelView ); | ||
it( 'should add ImageBalloonPanel to view body', () => { | ||
expect( panel ).to.be.instanceOf( ImageBalloonPanel ); | ||
} ); | ||
it( 'should attach toolbar when image is selected', () => { | ||
const spy = sinon.spy( panel, 'attachTo' ); | ||
it( 'should show the panel when editor gains focus and image is selected', () => { | ||
setData( doc, '[<image src=""></image>]' ); | ||
testPanelAttach( spy ); | ||
} ); | ||
editor.ui.focusTracker.isFocused = false; | ||
const spy = sinon.spy( plugin, 'show' ); | ||
editor.ui.focusTracker.isFocused = true; | ||
it( 'should calculate panel position on scroll event', () => { | ||
setData( doc, '[<image src=""></image>]' ); | ||
const spy = sinon.spy( panel, 'attachTo' ); | ||
global.window.dispatchEvent( new Event( 'scroll' ) ); | ||
testPanelAttach( spy ); | ||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
it( 'should calculate panel position on resize event', () => { | ||
it( 'should not show the panel automatically when it is disabled', () => { | ||
plugin.isEnabled = false; | ||
setData( doc, '[<image src=""></image>]' ); | ||
const spy = sinon.spy( panel, 'attachTo' ); | ||
editor.ui.focusTracker.isFocused = true; | ||
const spy = sinon.spy( plugin, 'show' ); | ||
global.window.dispatchEvent( new Event( 'resize' ) ); | ||
editingView.render(); | ||
testPanelAttach( spy ); | ||
} ); | ||
it( 'should not calculate panel position on scroll if no image is selected', () => { | ||
setData( doc, '<image src=""></image>' ); | ||
const spy = sinon.spy( panel, 'attachTo' ); | ||
global.window.dispatchEvent( new Event( 'scroll' ) ); | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
it( 'should not calculate panel position on resize if no image is selected', () => { | ||
setData( doc, '<image src=""></image>' ); | ||
const spy = sinon.spy( panel, 'attachTo' ); | ||
it( 'should not show the panel when editor looses focus', () => { | ||
editor.ui.focusTracker.isFocused = true; | ||
const spy = sinon.spy( plugin, 'show' ); | ||
editor.ui.focusTracker.isFocused = false; | ||
global.window.dispatchEvent( new Event( 'resize' ) ); | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
it( 'should hide the panel when editor looses focus', () => { | ||
setData( doc, '[<image src=""></image>]' ); | ||
editor.ui.focusTracker.isFocused = true; | ||
const spy = sinon.spy( panel, 'hide' ); | ||
editor.ui.focusTracker.isFocused = false; | ||
it( 'should detach panel with hide() method', () => { | ||
const spy = sinon.spy( panel, 'detach' ); | ||
plugin.hide(); | ||
@@ -148,36 +140,2 @@ sinon.assert.calledOnce( spy ); | ||
// Returns BalloonPanelView from provided collection. | ||
function getBalloonPanelView( viewCollection ) { | ||
return viewCollection.find( item => item instanceof BalloonPanelView ); | ||
} | ||
// Tests if panel.attachTo() was called correctly. | ||
function testPanelAttach( spy ) { | ||
const domRange = editor.editing.view.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ); | ||
sinon.assert.calledOnce( spy ); | ||
const options = spy.firstCall.args[ 0 ]; | ||
// Check if proper range was used. | ||
expect( options.target.startContainer ).to.equal( domRange.startContainer ); | ||
expect( options.target.startOffset ).to.equal( domRange.startOffset ); | ||
expect( options.target.endContainer ).to.equal( domRange.endContainer ); | ||
expect( options.target.endOffset ).to.equal( domRange.endOffset ); | ||
// Check if north/south calculation is correct. | ||
const [ north, south ] = options.positions; | ||
const targetRect = { top: 10, left: 20, width: 200, height: 100, bottom: 110, right: 220 }; | ||
const balloonRect = { width: 50, height: 20 }; | ||
const northPosition = north( targetRect, balloonRect ); | ||
expect( northPosition.name ).to.equal( 'n' ); | ||
expect( northPosition.top ).to.equal( targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset ); | ||
expect( northPosition.left ).to.equal( targetRect.left + targetRect.width / 2 - balloonRect.width / 2 ); | ||
const southPosition = south( targetRect, balloonRect ); | ||
expect( southPosition.name ).to.equal( 's' ); | ||
expect( southPosition.top ).to.equal( targetRect.bottom + BalloonPanelView.arrowVerticalOffset ); | ||
expect( southPosition.left ).to.equal( targetRect.left + targetRect.width / 2 - balloonRect.width / 2 ); | ||
} | ||
// Plugin that adds fake_button to editor's component factory. | ||
@@ -184,0 +142,0 @@ class FakeButton extends Plugin { |
@@ -6,2 +6,2 @@ ## ImageStyle feature | ||
* Click on "Full size image" icon. Image should be back to its original state. | ||
* When image toolbar is visible, resize the browser window and scroll - check if toolbar is placed in proper position. | ||
* Resize the browser window so the scrollbar is visible. Click on image and scroll editor contents - check if toolbar is placed in proper position. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
169229
10
57
3347
5
5
+ Added@ckeditor/ckeditor5-utils@*
+ Added@ckeditor/ckeditor5-theme-lark@43.2.0(transitive)