Socket
Socket
Sign inDemoInstall

@ckeditor/ckeditor5-image

Package Overview
Dependencies
Maintainers
1
Versions
646
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-image - npm Package Compare versions

Comparing version 0.2.0 to 0.3.0

src/imagealternatetext/imagealternatetext.js

6

lang/contexts.json
{
"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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc