ckeditor5-1.0.x-dev/js/drupal/src/drupalImage/src/drupalimageediting.js
js/drupal/src/drupalImage/src/drupalimageediting.js
import { Plugin } from 'ckeditor5/src/core';
export default class DrupalImageEditing extends Plugin {
/**
* @inheritdoc
*/
static get pluginName() {
return 'DrupalImageEditing';
}
/**
* @inheritdoc
*/
init() {
const editor = this.editor;
const conversion = editor.conversion;
const { schema } = editor.model;
if ( schema.isRegistered( 'imageInline' ) ) {
schema.extend( 'imageInline', {
allowAttributes: [
'dataEntityUuid',
'dataEntityType',
'width',
'height',
]
} );
}
if ( schema.isRegistered( 'imageBlock' ) ) {
schema.extend( 'imageBlock', {
allowAttributes: [
'dataEntityUuid',
'dataEntityType',
'width',
'height',
]
} );
}
// Conversion.
conversion.for( 'upcast' )
.add( viewImageToModelImage( editor ) )
.attributeToAttribute( {
view: {
name: 'img',
key: 'width',
},
model: {
key: 'width',
value: viewElement => {
return viewElement.getAttribute('width') + 'px';
}
}
} )
.attributeToAttribute( {
view: {
name: 'img',
key: 'height',
},
model: {
key: 'height',
value: viewElement => {
return viewElement.getAttribute('height') + 'px';
}
}
});
conversion.for( 'downcast' )
.add( modelEntityUuidToDataAttribute() )
.add( modelEntityTypeToDataAttribute() );
conversion.for( 'dataDowncast' )
.add( dispatcher => {
dispatcher.on( 'insert:caption', ( evt, data, conversionApi ) => {
if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
return;
}
let captionText = '';
for ( const { item } of editor.model.createRangeIn( data.item ) ) {
if ( !conversionApi.consumable.consume( item, 'insert' ) ) {
continue;
}
if ( item.is( '$textProxy' ) ) {
captionText += item.data;
}
}
if ( captionText ) {
const imageViewElement = conversionApi.mapper.toViewElement( data.item.parent );
conversionApi.writer.setAttribute( 'data-caption', captionText, imageViewElement );
}
},
{ priority: 'high' }
);
} )
.add( dispatcher => {
dispatcher.on( 'insert:$text', ( evt, data ) => {
const { parent } = data.item;
const isInImageCaption = parent.is( 'element', 'caption' ) && parent.parent.is( 'element', 'imageBlock' );
if ( isInImageCaption ) {
// Prevent `modelViewSplitOnInsert()` function inside
// ckeditor5-list package from interfering when downcasting a text
// inside caption. Normally aforementioned function tries to
// mitigate side effects of inserting content in the middle of the
// lists, but in this case we want to stop the conversion from
// proceeding.
evt.stop();
}
},
// Make sure we are overriding the `modelViewSplitOnInsert() converter
// from ckeditor5-list.
{ priority: 'highest' }
);
} )
.elementToElement( {
model: 'imageBlock',
view: ( modelElement, { writer } ) => createImageViewElement( writer, 'imageBlock' ),
converterPriority: 'high'
} )
.elementToElement( {
model: 'imageInline',
view: ( modelElement, { writer } ) => createImageViewElement( writer, 'imageInline' ),
converterPriority: 'high'
} )
.add( modelImageStyleToDataAttribute() )
.add ( modelImageWidthToAttribute() )
.add ( modelImageHeightToAttribute() );
}
}
function viewImageToModelImage( editor ) {
return dispatcher => {
dispatcher.on( 'element:img', converter, { priority: 'high' } );
};
function converter( evt, data, conversionApi ) {
const { viewItem } = data;
const { writer, consumable, safeInsert, updateConversionResult, schema } = conversionApi;
const attributesToConsume = [];
let image;
// Not only check if a given `img` view element has been consumed, but also
// verify it has `src` attribute present.
if ( !consumable.test( viewItem, { name: true, attributes: 'src' } ) ) {
return;
}
// Create image that's allowed in the given context.
if ( schema.checkChild( data.modelCursor, 'imageInline' ) ) {
image = writer.createElement( 'imageInline', { src: viewItem.getAttribute( 'src' ) } );
} else {
image = writer.createElement( 'imageBlock', { src: viewItem.getAttribute( 'src' ) } );
}
if ( editor.plugins.has( 'ImageStyleEditing' ) &&
consumable.test( viewItem, { name: true, attributes: 'data-align' } )
) {
// https://ckeditor.com/docs/ckeditor5/latest/api/module_image_imagestyle_utils.html#constant-defaultStyles
const dataToPresentationMapBlock = {
left: 'alignBlockLeft',
center: 'alignCenter',
right: 'alignBlockRight'
};
const dataToPresentationMapInline = {
left: 'alignLeft',
right: 'alignRight'
};
const dataAlign = viewItem.getAttribute( 'data-align' );
const alignment = image.is( 'element', 'imageBlock' ) ?
dataToPresentationMapBlock[ dataAlign ] :
dataToPresentationMapInline[ dataAlign ];
writer.setAttribute( 'imageStyle', alignment, image );
// Make sure the attribute can be consumed after successful `safeInsert`
// operation.
attributesToConsume.push( 'data-align' );
}
// Check if the view element has still unconsumed `data-caption` attribute.
// Also, we can add caption only to block image.
if ( image.is( 'element', 'imageBlock' ) &&
consumable.test( viewItem, { name: true, attributes: 'data-caption' } )
) {
// Create `caption` model element. Thanks to that element the rest of the
// `ckeditor5-plugin` converters can recognize this image as a block image
// with a caption.
const caption = writer.createElement( 'caption' );
writer.insertText( viewItem.getAttribute( 'data-caption' ), caption );
// Insert the caption element into image, as a last child.
writer.append( caption, image );
// Make sure the attribute can be consumed after successful `safeInsert`
// operation.
attributesToConsume.push( 'data-caption' );
}
if ( consumable.test( viewItem, { name: true, attributes: 'data-entity-uuid' } ) ) {
writer.setAttribute( 'dataEntityUuid', viewItem.getAttribute( 'data-entity-uuid' ), image );
attributesToConsume.push( 'data-entity-uuid' );
}
if ( consumable.test( viewItem, { name: true, attributes: 'data-entity-type' } ) ) {
writer.setAttribute( 'dataEntityFile', viewItem.getAttribute( 'data-entity-type' ), image );
attributesToConsume.push( 'data-entity-type' );
}
// Try to place the image in the allowed position.
if ( !safeInsert( image, data.modelCursor ) ) {
return;
}
// Mark given element as consumed. Now other converters will not process it
// anymore.
consumable.consume( viewItem, { name: true, attributes: attributesToConsume } );
// Make sure `modelRange` and `modelCursor` is up to date after inserting
// new nodes into the model.
updateConversionResult( image, data );
}
}
function createImageViewElement( writer ) {
return writer.createEmptyElement( 'img' );
}
function modelEntityUuidToDataAttribute() {
return dispatcher => {
dispatcher.on( 'attribute:dataEntityUuid', converter );
};
function converter( evt, data, conversionApi ) {
const { item } = data;
const { consumable, writer } = conversionApi;
if ( !consumable.consume( item, evt.name ) ) {
return;
}
const viewElement = conversionApi.mapper.toViewElement( item );
const imageInFigure = Array.from( viewElement.getChildren() ).find( child => child.name === 'img' );
writer.setAttribute( 'data-entity-uuid', data.attributeNewValue, imageInFigure || viewElement );
}
}
function modelEntityTypeToDataAttribute() {
return dispatcher => {
dispatcher.on( 'attribute:dataEntityType', converter );
};
function converter( evt, data, conversionApi ) {
const { item } = data;
const { consumable, writer } = conversionApi;
if ( !consumable.consume( item, evt.name ) ) {
return;
}
const viewElement = conversionApi.mapper.toViewElement( item );
const imageInFigure = Array.from( viewElement.getChildren() ).find( child => child.name === 'img' );
writer.setAttribute( 'data-entity-type', data.attributeNewValue, imageInFigure || viewElement );
}
}
function modelImageStyleToDataAttribute() {
return dispatcher => {
dispatcher.on( 'attribute:imageStyle', converter, { priority: 'high' } );
};
function converter( evt, data, conversionApi ) {
const { item } = data;
const { consumable, writer } = conversionApi;
const mapping = {
alignLeft: 'left',
alignRight: 'right',
alignCenter: 'center',
alignBlockRight: 'right',
alignBlockLeft: 'left',
};
// Consume only for the values that can be converted into data-align.
if ( !mapping[data.attributeNewValue] || !consumable.consume( item, evt.name ) ) {
return;
}
const viewElement = conversionApi.mapper.toViewElement( item );
const imageInFigure = Array.from( viewElement.getChildren() ).find( child => child.name === 'img' );
writer.setAttribute( 'data-align', mapping[data.attributeNewValue], imageInFigure || viewElement );
}
}
function modelImageWidthToAttribute() {
return dispatcher => {
dispatcher.on('attribute:width:imageInline', converter, { priority: 'high' });
dispatcher.on('attribute:width:imageBlock', converter, { priority: 'high' });
};
function converter(evt, data, conversionApi) {
const { item } = data;
const { consumable, writer } = conversionApi;
if (!consumable.consume(item, evt.name) ) {
return;
}
const viewElement = conversionApi.mapper.toViewElement(item);
const imageInFigure = Array.from(viewElement.getChildren()).find(child => child.name === 'img');
writer.setAttribute('width', data.attributeNewValue.replace('px', ''), imageInFigure || viewElement);
}
}
function modelImageHeightToAttribute() {
return dispatcher => {
dispatcher.on('attribute:height:imageInline', converter, { priority: 'high' });
dispatcher.on('attribute:height:imageBlock', converter, { priority: 'high' });
};
function converter(evt, data, conversionApi) {
const { item } = data;
const { consumable, writer } = conversionApi;
if (!consumable.consume(item, evt.name) ) {
return;
}
const viewElement = conversionApi.mapper.toViewElement(item);
const imageInFigure = Array.from(viewElement.getChildren()).find(child => child.name === 'img');
writer.setAttribute('height', data.attributeNewValue.replace('px', ''), imageInFigure || viewElement);
}
}
